nextjs - ✅(Solved) Fix Custom `nextConfig.pageExtensions` breaks `instrumentation.ts` [1 pull requests, 1 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#92342Fetched 2026-04-08 02:43:18
View on GitHub
Comments
0
Participants
1
Timeline
4
Reactions
0
Author
Participants
Timeline (top)
labeled ×3issue_type_added ×1

PR fix notes

PR #92934: build: detect instrumentation/middleware/proxy with multi-segment pageExtensions

Description (problem / solution / changelog)

What?

Make next build detect instrumentation, middleware, and proxy files when the user configures a multi-segment pageExtensions entry (e.g. 'universal.ts') and names their hook accordingly:

// next.config.js
module.exports = {
  pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'universal.ts', 'universal.tsx'],
}
// src/instrumentation.universal.ts
export function register() { /* ... */ }

Today this hook is silently dropped — the file is never compiled to .next/server/instrumentation.js and register() never runs. Same for middleware.universal.ts and proxy.universal.ts.

Why?

packages/next/src/build/index.ts builds three regexes from config.pageExtensions and uses them to filter root-level files into rootPaths. That part works — instrumentation.universal.ts correctly survives the filter (the unescaped . in the extension list is treated as \.|., which still matches the literal . in the filename).

The bug is in the bucketing loop right after, which iterates rootPaths and assigns each match to one of the three target paths via:

const { name: fileBaseName } = path.parse(rootPath)
if (fileBaseName === INSTRUMENTATION_HOOK_FILENAME) instrumentationHookFilePath = rootPath

path.parse('/src/instrumentation.universal.ts').name is 'instrumentation.universal' — Node only strips the last extension. So the equality check fails and the file is dropped on the floor. The filter said yes, the bucket said no.

The same pattern applies to MIDDLEWARE_FILENAME and PROXY_FILENAME, so any user with proxy.<custom>.ts / middleware.<custom>.ts hits the identical bug.

How?

Reuse the three detection regexes that already filter rootPaths to also assign each matched file to its bucket, comparing against path.parse(rootPath).base (the full filename including extension). The filter and the bucketing now agree on the same matching rule:

if (middlewareDetectionRegExp.test(fileBase))            middlewareFilePath = rootPath
else if (proxyDetectionRegExp.test(fileBase))            proxyFilePath = rootPath
else if (instrumentationHookDetectionRegExp.test(fileBase)) instrumentationHookFilePath = rootPath

Behavior with the default pageExtensions (['ts', 'tsx', 'js', 'jsx', 'mts', 'mjs', ...]) is unchanged — those are all single-segment, and a file named instrumentation.ts matches the regex and lands in the bucket exactly as before.

Scope

This fixes the detection gap reported as the primary issue (instrumentation never registered with custom multi-segment pageExtensions). The reporter also notes a secondary observation in their case 3 (adding proxy.universal.ts makes instrumentation register but with NEXT_RUNTIME=edge). That smells like a separate downstream runtime-routing issue and is out of scope here — once detection works correctly with this PR, that scenario is worth re-evaluating against canary.

Test plan

Added test/integration/instrumentation-page-extensions/:

  • Fixture: project with pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'universal.ts', 'universal.tsx'], src/instrumentation.universal.ts exporting register(), and a minimal pages/index.tsx.
  • Test: runs nextBuild, asserts exit code 0 and that .next/server/instrumentation.js exists — the latter only happens if detection succeeded.
  • Webpack-only (gated on IS_TURBOPACK_TEST), mirroring the existing build-trace-extra-entries integration suite.

Verified locally on Windows + webpack: test fails on canary without the fix and passes after it.

Fixes #92342

Changed files

  • packages/next/src/build/index.ts (modified, +12/-9)
  • test/integration/instrumentation-page-extensions/app/next.config.js (added, +4/-0)
  • test/integration/instrumentation-page-extensions/app/src/instrumentation.universal.ts (added, +5/-0)
  • test/integration/instrumentation-page-extensions/app/src/pages/index.tsx (added, +3/-0)
  • test/integration/instrumentation-page-extensions/app/tsconfig.json (added, +19/-0)
  • test/integration/instrumentation-page-extensions/test/index.test.ts (added, +36/-0)

Code Example

const nextConfig: NextConfig = {
  output: 'standalone',
};

---

export function register() {
  // @ts-ignore
  const runtime = typeof EdgeRuntime === 'string' ? 'edge' : 'nodejs';
  console.log(`[instrumentation] register() called in ${runtime} (NEXT_RUNTIME=${process.env.NEXT_RUNTIME})`);
}

---

npm i && rm -rf .next && docker compose up --build --force-recreate

---

const nextConfig: NextConfig = {
  output: 'standalone',
  pageExtensions: ['tsx', 'ts', 'universal.ts', 'universal.tsx'],
};

---

npm i && rm -rf .next && docker compose up --build --force-recreate

---

npm i && rm -rf .next && docker compose up --build --force-recreate

---

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 24.6.0: Mon Jul 14 11:30:40 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T6041
  Available memory (MB): 49152
  Available CPU cores: 12
Binaries:
  Node: 24.2.0
  npm: 11.3.0
  Yarn: N/A
  pnpm: N/A
Relevant Packages:
  next: 16.0.10 // There is a newer version (16.2.2) available, upgrade recommended! 
  eslint-config-next: N/A
  react: 19.2.3
  react-dom: 19.2.3
  typescript: 5.9.3
Next.js Config:
  output: standalone
There is a newer version (16.2.2) available, upgrade recommended! 
   Please try the latest canary version (`npm install next@canary`) to confirm the issue still exists before creating a new issue.
   Read more - https://nextjs.org/docs/messages/opening-an-issue
Deployment: output: 'standalone', Docker
RAW_BUFFERClick to expand / collapse

Link to the code that reproduces this issue

https://github.com/Git-I985/next-custom-extensions-breaks-instrumentation

To Reproduce

Baseline (works correctly)

next.config.ts — no pageExtensions:

const nextConfig: NextConfig = {
  output: 'standalone',
};

src/instrumentation.ts:

export function register() {
  // @ts-ignore
  const runtime = typeof EdgeRuntime === 'string' ? 'edge' : 'nodejs';
  console.log(`[instrumentation] register() called in ${runtime} (NEXT_RUNTIME=${process.env.NEXT_RUNTIME})`);
}
npm i && rm -rf .next && docker compose up --build --force-recreate

Result: register() is called, logs NEXT_RUNTIME=nodejs. ✅


Reproducing the bug

next.config.ts — add custom pageExtensions:

const nextConfig: NextConfig = {
  output: 'standalone',
  pageExtensions: ['tsx', 'ts', 'universal.ts', 'universal.tsx'],
};

Rename src/instrumentation.tssrc/instrumentation.universal.ts (same content).

npm i && rm -rf .next && docker compose up --build --force-recreate

Result: register() is never called. ❌

Now add proxy.universal.ts at the project root or src folder

npm i && rm -rf .next && docker compose up --build --force-recreate

Result: register() is called, but logs NEXT_RUNTIME=edge (why not node?). ❌

Current vs. Expected behavior

ScenarioCurrentExpected
instrumentation.ts (default ext)✅ Called with NEXT_RUNTIME=nodejs
instrumentation.universal.ts, no proxy❌ Not called at allShould be called with NEXT_RUNTIME=nodejs
instrumentation.universal.ts + proxy.universal.ts❌ Called, but NEXT_RUNTIME=edgeShould be called with NEXT_RUNTIME=nodejs
instrumentation.universal.ts + proxy.ts❌ Not called (.ts is in pageExtensions)Should be called

Expected: The file extension used for instrumentation should have no effect on:

  1. Whether the file is executed
  2. Which runtime (NEXT_RUNTIME) it executes in

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 24.6.0: Mon Jul 14 11:30:40 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T6041
  Available memory (MB): 49152
  Available CPU cores: 12
Binaries:
  Node: 24.2.0
  npm: 11.3.0
  Yarn: N/A
  pnpm: N/A
Relevant Packages:
  next: 16.0.10 // There is a newer version (16.2.2) available, upgrade recommended! 
  eslint-config-next: N/A
  react: 19.2.3
  react-dom: 19.2.3
  typescript: 5.9.3
Next.js Config:
  output: standalone
 ⚠ There is a newer version (16.2.2) available, upgrade recommended! 
   Please try the latest canary version (`npm install next@canary`) to confirm the issue still exists before creating a new issue.
   Read more - https://nextjs.org/docs/messages/opening-an-issue
Deployment: output: 'standalone', Docker

Which area(s) are affected? (Select all that apply)

Instrumentation, Middleware, Runtime, Not sure

Which stage(s) are affected? (Select all that apply)

next build (local), Other (Deployed)

Additional context

Kinda two distinct but related bugs are observed:

Bug 1 — Extension affects proxy requirement. With a custom extension, instrumentation.universal.ts is silently ignored unless a matching proxy.universal.ts is present. Meanwhile, proxy.ts does not work as a trigger even though .ts is explicitly listed in pageExtensions.

Bug 2 — Extension forces Edge runtime. Once the proxy file is in place and instrumentation.universal.ts is executed, NEXT_RUNTIME is always edge — regardless of the actual server runtime context. Standard instrumentation.ts correctly reports NEXT_RUNTIME=nodejs.

The pageExtensions config is documented as controlling which file extensions are treated as pages/routes. It should have no side-effects on the instrumentation lifecycle or the runtime environment reported to register().

extent analysis

TL;DR

Upgrading to the latest version of Next.js may resolve the issues related to custom page extensions affecting instrumentation and runtime detection.

Guidance

  • The problem seems to be related to the interaction between custom page extensions and instrumentation in Next.js.
  • Try upgrading to the latest version of Next.js (16.2.2) as recommended in the environment information section.
  • Verify that the issue persists after upgrading by running the same reproduction steps.
  • If the issue is still present, try using the next@canary version to confirm if it's a known issue that has been fixed in the latest development version.

Example

No specific code example is provided as the issue seems to be related to the configuration and version of Next.js rather than a specific code snippet.

Notes

The provided information suggests that there are two distinct bugs: one related to the requirement of a proxy file with a custom extension and another related to the incorrect detection of the runtime environment. Upgrading to the latest version of Next.js may resolve these issues.

Recommendation

Upgrade to the latest version of Next.js (16.2.2) to potentially resolve the issues related to custom page extensions and instrumentation. This is recommended because the current version (16.0.10) may have known issues that have been fixed in later versions.

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…

Still need to ship something?

×6

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

Back to top recommendations

TRENDING