next.js icon indicating copy to clipboard operation
next.js copied to clipboard

Next.js 15.4.4+ production builds broken when using a specific 3rd-party-library - dev server is fine

Open soulchild opened this issue 5 months ago • 37 comments

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

soulchild avatar Aug 14 '25 07:08 soulchild

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.

Image

icyJoseph avatar Aug 14 '25 08:08 icyJoseph

Good catch, @icyJoseph! Judging by the Next.js 15.4.4 changelog, swc has been upgraded. Could this have something to do with it?

soulchild avatar Aug 14 '25 08:08 soulchild

Debugging right now :) just a sec

icyJoseph avatar Aug 14 '25 08:08 icyJoseph

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.

icyJoseph avatar Aug 14 '25 13:08 icyJoseph

Awesome, really appreciate you looking into this!

soulchild avatar Aug 14 '25 14:08 soulchild

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

icyJoseph avatar Aug 14 '25 21:08 icyJoseph

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...

icyJoseph avatar Aug 14 '25 22:08 icyJoseph

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 avatar Aug 14 '25 23:08 icyJoseph

@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. 😄

soulchild avatar Aug 19 '25 09:08 soulchild

Any updates here, @icyJoseph? Is the plan to escalate this to the SWC devs?

soulchild avatar Sep 02 '25 12:09 soulchild

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.

icyJoseph avatar Sep 02 '25 14:09 icyJoseph

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.

icyJoseph avatar Sep 03 '25 20:09 icyJoseph

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.

icyJoseph avatar Sep 08 '25 18:09 icyJoseph

Maybe dynamics-web-api does something "special" during bundling which trips up swc? It seems to be using esbuild to create the distributed bundle.

soulchild avatar Sep 09 '25 09:09 soulchild

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 avatar Sep 09 '25 10:09 icyJoseph

@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 👻

soulchild avatar Sep 17 '25 08:09 soulchild

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.

soulchild avatar Sep 17 '25 08:09 soulchild

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...

icyJoseph avatar Sep 17 '25 11:09 icyJoseph

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",
});

soulchild avatar Sep 17 '25 12:09 soulchild

Right, any additional reference to the function preserves it, but I cannot trigger this failed minification in an isolated setting. We are getting close...

icyJoseph avatar Sep 17 '25 13:09 icyJoseph

I'm trimming down the library to the bare minimum right now. Will report back soon.

soulchild avatar Sep 17 '25 14:09 soulchild

@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.

soulchild avatar Sep 17 '25 15:09 soulchild

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 avatar Sep 17 '25 19:09 icyJoseph

@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.

soulchild avatar Sep 18 '25 07:09 soulchild

We also got this issue, https://github.com/SiroSuzume/next-build-bug-example/tree/main - what's happening here 😱

icyJoseph avatar Sep 18 '25 08:09 icyJoseph

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)

😱

soulchild avatar Sep 18 '25 08:09 soulchild

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.

soulchild avatar Sep 18 '25 09:09 soulchild

So like, what's happening here...

Seems oddly similar right? url ends up undefined

icyJoseph avatar Sep 18 '25 12:09 icyJoseph

Seems oddly similar right? url ends 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.

soulchild avatar Sep 18 '25 13:09 soulchild

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

icyJoseph avatar Sep 18 '25 17:09 icyJoseph