Next.js 15.4.4+ production builds broken when using a specific 3rd-party-library - dev server is fine
Link to the code that reproduces this issue
https://github.com/soulchild/next-dynamics-web-api-reproduction
To Reproduce
The repo contains the exact same example, once with Next.js 15.4.3 (working as intended) and Next.js 15.4.6 (build breaks with a weird error). To reproduce, run npm i && npm run build in both next-15.4.3 and next-15.4.6.
The issue is not encountered when using the development server with npm run dev. Both versions show the same (correct) "authentication error". Enabling TurboPack for the production build doesn't help. Disabling TurboPack for the development server also doesn't suddenly introduce the error for the Next.js 15.4.3 example.
Current vs. Expected behavior
next.js-15.4.3
The result of the build is the following, correctly showing that the credentials are wrong/missing, proving that in general the communication works:
> next build
▲ Next.js 15.4.3
Creating an optimized production build ...
✓ Compiled successfully in 0ms
Skipping validation of types
Skipping linting
✓ Collecting page data
Error occurred prerendering page "/". Read more: https://nextjs.org/docs/messages/prerender-error
Error: Unexpected Error
at a.handleHttpError (.next/server/app/page.js:5:11637)
at IncomingMessage.<anonymous> (.next/server/app/page.js:5:18823) {
status: 401,
statusText: '',
statusMessage: 'Unauthorized',
headers: [Object],
digest: '1482060732'
}
next.js-15.4.6
The result shows a rather obscure error, indicating that the Next.js build somehow broke the external dynamics-web-api library. This started happening with Next.js 15.4.4.
> next build
▲ Next.js 15.4.6
Creating an optimized production build ...
✓ Compiled successfully in 0ms
Skipping validation of types
Skipping linting
✓ Collecting page data
Error occurred prerendering page "/". Read more: https://nextjs.org/docs/messages/prerender-error
TypeError: Cannot read properties of undefined (reading 'toString')
at a3 (.next/server/app/page.js:13:2043)
at async aZ (.next/server/app/page.js:13:257) {
digest: '2393984709'
}
Provide environment information
Operating System:
Platform: darwin
Arch: arm64
Version: Darwin Kernel Version 24.6.0: Mon Jul 14 11:30:29 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T6000
Available memory (MB): 32768
Available CPU cores: 10
Binaries:
Node: 24.2.0
npm: 11.3.0
Yarn: 1.22.22
pnpm: 10.14.0
Relevant Packages:
next: 15.4.6 // Latest available version is detected (15.4.6).
eslint-config-next: 15.4.6
react: 19.1.0
react-dom: 19.1.0
typescript: 5.9.2
Next.js Config:
output: standalone
Which area(s) are affected? (Select all that apply)
Not sure
Which stage(s) are affected? (Select all that apply)
next build (local)
Additional context
No response
Looks like a server minification issue. Running the 15.4.6 version with:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
reactStrictMode: true,
output: "standalone",
eslint: {
ignoreDuringBuilds: true,
},
typescript: { ignoreBuildErrors: true },
experimental: {
serverMinification: false
}
};
export default nextConfig;
"Passes" the build, with the previous error.
Good catch, @icyJoseph! Judging by the Next.js 15.4.4 changelog, swc has been upgraded. Could this have something to do with it?
Debugging right now :) just a sec
I had to switch away to something else, but now, after building next-swc and installing the entire thing in your project, yeah it look slike the update to swc v33 could be the issue - I though the next commit would be, but it looks like it is v33 - I'll redo this check later tonight.
Awesome, really appreciate you looking into this!
Ok ~ so, I did confirm that, 6d0ffcc752cccdd0637783ca9325cbfc2fbb84dc starts to trigger the .toString() issue.
I think this is the area where things are being minified incorrectly:
- https://github.com/AleksandrRogov/DynamicsWebApi/blob/506ecef31cf857d8a8df44c6c53ac54f091b1689/src/client/RequestClient.ts#L166-L170
The thing here is that we should be able to produce a simple, or rather minimal pattern, that we can take to an SWC playground and show the error in minification ~ let's try before we ask for help over at SWC
One more data drop:
Without minification
{
request: {
functionName: 'callFunction',
method: 'GET',
addPath: 'WhoAmI()',
queryParams: undefined,
_isUnboundRequest: true,
isBatch: false,
responseParameters: { convertedToBatch: false },
userHeaders: undefined,
collection: undefined,
path: 'WhoAmI()',
async: true,
headers: {}
},
config: {
serverUrl: 'https://test.api.crm4.dynamics.com',
impersonate: null,
impersonateAAD: null,
onTokenRefresh: [AsyncFunction: onTokenRefresh],
includeAnnotations: null,
maxPageSize: null,
returnRepresentation: null,
proxy: null,
dataApi: {
path: 'data',
version: '9.1',
url: 'https://test.api.crm4.dynamics.com/api/data/v9.1/'
},
searchApi: {
path: 'search',
version: '1.0',
url: 'https://test.api.crm4.dynamics.com/api/search/v1.0/'
},
serviceApi: { url: 'https://test.api.crm4.dynamics.com/api/' }
}
}
With minification
{
request: {
functionName: 'callFunction',
method: 'GET',
addPath: 'WhoAmI()',
queryParams: undefined,
_isUnboundRequest: true,
isBatch: false,
responseParameters: { convertedToBatch: false },
userHeaders: undefined,
collection: undefined,
path: 'WhoAmI()',
async: true,
headers: {}
},
config: {
serverUrl: 'https://test.api.crm4.dynamics.com',
impersonate: null,
impersonateAAD: null,
onTokenRefresh: [AsyncFunction: onTokenRefresh],
includeAnnotations: null,
maxPageSize: null,
returnRepresentation: null,
proxy: null,
dataApi: { path: 'data', version: '9.1', url: undefined },
searchApi: { path: 'search', version: '1.0', url: undefined },
serviceApi: { url: undefined }
}
}
For some reason those url properties are undefined in config.dataApi and others...
And now I think the issue is in mergeConfig, https://github.com/AleksandrRogov/DynamicsWebApi/blob/dev/src/utils/Config.ts#L77
server minification off
before
{
internalConfig: {
serverUrl: null,
impersonate: null,
impersonateAAD: null,
onTokenRefresh: null,
includeAnnotations: null,
maxPageSize: null,
returnRepresentation: null,
proxy: null,
dataApi: { path: 'data', version: '9.2', url: '' },
searchApi: { path: 'search', version: '1.0', url: '' },
serviceApi: { url: '' }
},
config: {
serverUrl: 'https://test.api.crm4.dynamics.com',
dataApi: { version: '9.1' },
onTokenRefresh: [AsyncFunction: onTokenRefresh]
}
}
after
{
internalConfig: {
serverUrl: 'https://test.api.crm4.dynamics.com',
impersonate: null,
impersonateAAD: null,
onTokenRefresh: [AsyncFunction: onTokenRefresh],
includeAnnotations: null,
maxPageSize: null,
returnRepresentation: null,
proxy: null,
dataApi: {
path: 'data',
version: '9.1',
url: 'https://test.api.crm4.dynamics.com/api/data/v9.1/'
},
searchApi: {
path: 'search',
version: '1.0',
url: 'https://test.api.crm4.dynamics.com/api/search/v1.0/'
},
serviceApi: { url: 'https://test.api.crm4.dynamics.com/api/' }
},
config: {
serverUrl: 'https://test.api.crm4.dynamics.com',
dataApi: { version: '9.1' },
onTokenRefresh: [AsyncFunction: onTokenRefresh]
}
}
server minification on:
before
{
internalConfig: {
serverUrl: null,
impersonate: null,
impersonateAAD: null,
onTokenRefresh: null,
includeAnnotations: null,
maxPageSize: null,
returnRepresentation: null,
proxy: null,
dataApi: { path: 'data', version: '9.2', url: '' },
searchApi: { path: 'search', version: '1.0', url: '' },
serviceApi: { url: '' }
},
config: {
serverUrl: 'https://test.api.crm4.dynamics.com',
dataApi: { version: '9.1' },
onTokenRefresh: [AsyncFunction: onTokenRefresh]
}
}
after
{
internalConfig: {
serverUrl: 'https://test.api.crm4.dynamics.com',
impersonate: null,
impersonateAAD: null,
onTokenRefresh: [AsyncFunction: onTokenRefresh],
includeAnnotations: null,
maxPageSize: null,
returnRepresentation: null,
proxy: null,
dataApi: { path: 'data', version: '9.1', url: undefined },
searchApi: { path: 'search', version: '1.0', url: undefined },
serviceApi: { url: undefined }
},
config: {
serverUrl: 'https://test.api.crm4.dynamics.com',
dataApi: { version: '9.1' },
onTokenRefresh: [AsyncFunction: onTokenRefresh]
}
}
@icyJoseph I don't see the problem when using swc directly. I've added a reproduction to the repository, just run npm start. But I have absolutely no idea how and with what exact settings swc gets used by Next.js under the hood. 😄
Any updates here, @icyJoseph? Is the plan to escalate this to the SWC devs?
Hi, sorry a few things came about - I somehow missed the updates to the repository too - I see the SWC label was added - so they are likely aware of the issue too.
So here the problem is somehow the config merge step, if we could extract the code from there - we'd be 99% of the way there.
A little update, I tried to put all of the code necessary to do mergeConflict into an SWC playground, but no luck... I couldn't get that behavior where the url ends up undefined.
Maybe dynamics-web-api does something "special" during bundling which trips up swc? It seems to be using esbuild to create the distributed bundle.
I remember there was a way to get a hold of the code that's goes into the minifier - I'll investigate a bit more. I also tried your repo with a PR that updated SWC once again, but it was still broken there
@icyJoseph I dug a little deeper and found something strange: At the end of the chain to build/merge the config, the library calls the function getApiUrl. Starting with Next 15.4.6+ this function basically becomes a no-op and causes the URL to be undefined, thus breaking the library.
But here comes the funny part: When adding two console.log statements like this:
var getApiUrl = (serverUrl, apiConfig) => {
if (isRunningWithinPortals()) {
console.log('PORTALS');
return new URL("_api", global.window.location.origin).toString() + "/";
} else {
console.log('NOPORTALS');
if (!serverUrl) serverUrl = getClientUrl();
let url = "api";
if (apiConfig.path) {
url += `/${apiConfig.path}`;
}
if (apiConfig.version) {
url += `/v${apiConfig.version}`;
}
return new URL(url, serverUrl).toString() + "/";
}
};
… neither PORTALS nor NOPORTALS is logged.
To rule out isRunningWithinPortals() as the culprit, I replaced the condition with 1 === 2. Doesn't change anything. But if you remove all the stuff in the else branch, keeping just the console.log statements, you can see NOPORTALS being logged during the build.
Even better: If I remove the if condition, leaving only the else branch, everything works as well. WTF?
There's some spooky optimization shit going wrong here … or I'm already seeing 👻
Using 1 === 2 probably wasn't the best idea, because I can imagine that getting optimized away immediately. But it seems the optimization removes the else branch, too.
If I replace the condition with if (process.env.FOO === 'WTF') {} the function works correctly.
So, I guess 1 === 2 is optimized away in the same way as isRunningWithinPortals(), which is no wonder because it looks like this:
function isRunningWithinPortals() {
return false ? !!global.window.shell : false;
}
But during that process, the else branch is wiped as well, breaking the whole function.
This is a leap forward! Indeed some incorrect optimization is going on... 🤔 like if I try to introspect, by changing the var declaration to a function, and then .toString()-it to see its implementation, then things work, aka, build fails with 401... but if I remove that, we are back to failing... what...
Another piece for the puzzle: Explicitly exporting getApiUrl() and using it in the Next.js app also makes the library work:
node_modules/dynamics-web-api/dist/esm/dynamics-web-api.mjs
var DynamicsWebApi = _DynamicsWebApi;
export {
DynamicsWebApi,
+ getApiUrl
};
lib/dynamics.ts
import { DynamicsWebApi, getApiUrl } from "dynamics-web-api";
getApiUrl("https://test.api.crm4.dynamics.com", {
path: "data",
version: "9.1",
url: "",
});
export const dynamicsWebApi = new DynamicsWebApi({
serverUrl: "https://test.api.crm4.dynamics.com",
dataApi: {
version: "9.1",
},
onTokenRefresh: async () => "fakeToken",
});
Right, any additional reference to the function preserves it, but I cannot trigger this failed minification in an isolated setting. We are getting close...
I'm trimming down the library to the bare minimum right now. Will report back soon.
@icyJoseph Take a look at the bare branch of my repo. I've stripped down the dynamics library to a couple of lines and the problem persists.
Try npm run build in both next-* directories.
I have been trying to debug this, you can do NEXT_DEBUG_MINIFY=1 pnpm build and that can output a bit more info of minification.
We have an SWC update and the serverMinification flag that we know influence this issue, but what else could be at play here 🤔 - this is really odd
@icyJoseph I'm down to 7 lines still exhibiting the problem. Could we have a Next.js/SWC minification expert take a look at this? It's probably obvious to them at this point.
We also got this issue, https://github.com/SiroSuzume/next-build-bug-example/tree/main - what's happening here 😱
I added some debugging inside Next.js's minification plugin and inlined the miniDynamics() function in app/page.tsx:
app/page.tsx
import assert from "node:assert";
const miniDynamics = () => {
if (true) {
let url = "api";
url += "/";
return new URL(url, "https://example.com").toString();
}
};
assert(miniDynamics() === "https://example.com/api/");
export default function Home() {}
After building the app, the minification process seems to reduce the whole page component to pretty much this:
let d=require("node:assert");function e(){}c.n(d)()(!1)
😱
When I take the input that is fed into the minification process and run it through swc everything is fine. If I take the same input and run it through the minify() function of the Next.js swc module (which is what the minification plugin does), the function gets stripped away.
The swc module from Next v15.4.3 works correctly.
Seems oddly similar right?
urlends up undefined
Hot damn, I think that's it. And it all started with swc 1.13.1… Disabling the "compress" option (specifically, the "unused" option in the detailed settings) also makes the problem go away in later versions.
So what's the next step? Guess, I'll open a ticket in the swc project then.
A team member opened an issue too, https://github.com/swc-project/swc/issues/11102
It is my opinion that we keep both open issues, since on the surface they look different. Let's let the SWC maintainers decide how to proceed.
- https://github.com/vercel/next.js/issues/83925