nextjs - 💡(How to fix) Fix unstable_getResponseFromNextConfig does not support async function configs [1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
vercel/next.js#92499Fetched 2026-04-09 07:49:59
View on GitHub
Comments
1
Participants
2
Timeline
4
Reactions
0
Timeline (top)
closed ×1commented ×1labeled ×1locked ×1

unstable_getResponseFromNextConfig from next/experimental/testing/server does not support Next.js configs that are exported as async functions (the (phase, ctx) => Promise<NextConfig> pattern). It passes the config value directly to loadCustomRoutes, which accesses properties like config.rewrites and config.redirects immediately — without first checking if the config is a function and resolving it.

This means any next.config.mjs that uses a plugin returning an async function wrapper (e.g. withWorkflow, or any custom (phase, ctx) => config pattern) will silently produce null for all rewrite/redirect/header matches in tests.

Root Cause

In next/dist/experimental/testing/server/config-testing-utils.js:

async function unstable_getResponseFromNextConfig({ url, nextConfig, ... }) {
  // ...
  const routes = await loadCustomRoutes(nextConfig);
  //                                    ^^^^^^^^^
  // nextConfig may be an async (phase, ctx) => config function
  // but loadCustomRoutes expects a plain config object

loadCustomRoutes then accesses config.rewrites, config.assetPrefix, etc. directly. Since a function doesn't have these properties, everything resolves to undefined and no routes match.

Code Example

// next.config.mjs
import { withSomePlugin } from 'some-plugin';

const config = {
  async rewrites() {
    return [{ source: '/old', destination: '/new' }];
  },
};

// withSomePlugin returns async (phase, ctx) => resolvedConfig
export default withSomePlugin(config);

---

// test.ts
import { unstable_getResponseFromNextConfig, getRewrittenUrl } from 'next/experimental/testing/server';

const nextConfig = (await import('./next.config.mjs')).default;
// nextConfig is an async function, not a config object

const response = await unstable_getResponseFromNextConfig({
  url: '/old',
  nextConfig, // ← function, not object
});

getRewrittenUrl(response); // → null (expected: '/new')

---

async function unstable_getResponseFromNextConfig({ url, nextConfig, ... }) {
  // ...
  const routes = await loadCustomRoutes(nextConfig);
  //                                    ^^^^^^^^^
  // nextConfig may be an async (phase, ctx) => config function
  // but loadCustomRoutes expects a plain config object

---

let resolvedConfig = nextConfig;
if (typeof resolvedConfig === 'function') {
  resolvedConfig = await resolvedConfig('phase-production-build', { defaultConfig: {} });
}
const routes = await loadCustomRoutes(resolvedConfig);
RAW_BUFFERClick to expand / collapse

Description

unstable_getResponseFromNextConfig from next/experimental/testing/server does not support Next.js configs that are exported as async functions (the (phase, ctx) => Promise<NextConfig> pattern). It passes the config value directly to loadCustomRoutes, which accesses properties like config.rewrites and config.redirects immediately — without first checking if the config is a function and resolving it.

This means any next.config.mjs that uses a plugin returning an async function wrapper (e.g. withWorkflow, or any custom (phase, ctx) => config pattern) will silently produce null for all rewrite/redirect/header matches in tests.

Reproduction

// next.config.mjs
import { withSomePlugin } from 'some-plugin';

const config = {
  async rewrites() {
    return [{ source: '/old', destination: '/new' }];
  },
};

// withSomePlugin returns async (phase, ctx) => resolvedConfig
export default withSomePlugin(config);
// test.ts
import { unstable_getResponseFromNextConfig, getRewrittenUrl } from 'next/experimental/testing/server';

const nextConfig = (await import('./next.config.mjs')).default;
// nextConfig is an async function, not a config object

const response = await unstable_getResponseFromNextConfig({
  url: '/old',
  nextConfig, // ← function, not object
});

getRewrittenUrl(response); // → null (expected: '/new')

Root cause

In next/dist/experimental/testing/server/config-testing-utils.js:

async function unstable_getResponseFromNextConfig({ url, nextConfig, ... }) {
  // ...
  const routes = await loadCustomRoutes(nextConfig);
  //                                    ^^^^^^^^^
  // nextConfig may be an async (phase, ctx) => config function
  // but loadCustomRoutes expects a plain config object

loadCustomRoutes then accesses config.rewrites, config.assetPrefix, etc. directly. Since a function doesn't have these properties, everything resolves to undefined and no routes match.

Expected behavior

unstable_getResponseFromNextConfig should resolve function configs the same way Next.js does at build time — by calling the function with the appropriate phase and context before passing the result to loadCustomRoutes. Something like:

let resolvedConfig = nextConfig;
if (typeof resolvedConfig === 'function') {
  resolvedConfig = await resolvedConfig('phase-production-build', { defaultConfig: {} });
}
const routes = await loadCustomRoutes(resolvedConfig);

Version

[email protected] (but likely affects all versions since the testing utility was introduced)

extent analysis

TL;DR

To fix the issue, modify unstable_getResponseFromNextConfig to resolve async Next.js configs by calling them with the appropriate phase and context before passing the result to loadCustomRoutes.

Guidance

  • Check if the nextConfig is a function before passing it to loadCustomRoutes.
  • If it's a function, resolve it by calling it with the appropriate phase and context, such as 'phase-production-build' and an empty context object.
  • Use the resolved config object to load custom routes.
  • Verify the fix by testing getRewrittenUrl with an async Next.js config.

Example

async function unstable_getResponseFromNextConfig({ url, nextConfig, ... }) {
  let resolvedConfig = nextConfig;
  if (typeof resolvedConfig === 'function') {
    resolvedConfig = await resolvedConfig('phase-production-build', { defaultConfig: {} });
  }
  const routes = await loadCustomRoutes(resolvedConfig);
  // ...
}

Notes

This fix assumes that the async Next.js config function returns a promise that resolves to a plain config object. If the config function returns a different type of value, additional modifications may be needed.

Recommendation

Apply the workaround by modifying unstable_getResponseFromNextConfig to resolve async Next.js configs, as this is a necessary fix to support async configs in testing.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

unstable_getResponseFromNextConfig should resolve function configs the same way Next.js does at build time — by calling the function with the appropriate phase and context before passing the result to loadCustomRoutes. Something like:

let resolvedConfig = nextConfig;
if (typeof resolvedConfig === 'function') {
  resolvedConfig = await resolvedConfig('phase-production-build', { defaultConfig: {} });
}
const routes = await loadCustomRoutes(resolvedConfig);

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

nextjs - 💡(How to fix) Fix unstable_getResponseFromNextConfig does not support async function configs [1 comments, 2 participants]