nextjs - 💡(How to fix) Fix `@vercel/otel` fetch instrumentation stripped after HMR in dev mode — `resetFetch()` restores pre-instrumentation fetch [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#92877Fetched 2026-04-17 08:25:45
View on GitHub
Comments
0
Participants
1
Timeline
3
Reactions
0
Author
Participants
Timeline (top)
labeled ×3

Fix Action

Fix / Workaround

Timeline on cold start:
1. router-server.ts → originalFetch = globalThis.fetch   (raw fetch)
2. ensureInstrumentationRegistered() → registerOTel() → globalThis.fetch = otelWrapper
3. patchFetch() → wraps otelWrapper with Next.js cache layer
   Chain: user code → Next.js → OTel → real fetch  ✅

On every HMR event, resetFetch() restores globalThis.fetch = originalFetch — the pre-OTel raw fetch. Then patchFetch() re-wraps, but now wraps the raw fetch, permanently losing the OTel layer:

After HMR:
4. resetFetch() → globalThis.fetch = originalFetch  (pre-OTel)
5. patchFetch() → wraps raw fetch
   Chain: user code → Next.js → real fetch  ❌  (OTel wrapper lost)

Code Example

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 25.3.0
Binaries:
  Node: 22.21.1
  npm: 11.7.0
Relevant Packages:
  next: 16.2.3
  react: 19.2.5
  react-dom: 19.2.5
  typescript: 5.9.3
Next.js Config:
  output: N/A

---

Timeline on cold start:
1. router-server.ts → originalFetch = globalThis.fetch   (raw fetch)
2. ensureInstrumentationRegistered()registerOTel() → globalThis.fetch = otelWrapper
3. patchFetch() → wraps otelWrapper with Next.js cache layer
   Chain: user code → Next.jsOTel → real fetch  ✅

---

After HMR:
4. resetFetch() → globalThis.fetch = originalFetch  (pre-OTel)
5. patchFetch() → wraps raw fetch
   Chain: user code → Next.js → real fetch    (OTel wrapper lost)
RAW_BUFFERClick to expand / collapse

Link to the code that reproduces this issue

https://github.com/Strernd/otel-hmr-repro

To Reproduce

  1. Clone the reproduction repo and npm install
  2. Start the downstream header-logging server: npm run downstream
  3. Start Next.js dev server: npm run dev
  4. Open http://localhost:3000 — the page shows whether traceparent was received by the downstream server
  5. Check downstream terminal — traceparent header is present (works on cold start)
  6. Edit app/page.tsx (e.g. change the comment on line 1) to trigger HMR
  7. Refresh http://localhost:3000
  8. Check downstream terminal — traceparent is now MISSING

Current vs. Expected behavior

Expected: traceparent / tracestate headers propagated by @vercel/otel's propagateContextUrls should be present on every server-side fetch() in dev mode, including after HMR.

Actual: Headers are present on the first request after cold start but permanently disappear after any HMR event. They never come back until the dev server is fully restarted. Manual propagation.inject() still works, confirming OTel context is active — only the automatic fetch wrapper is lost.

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 25.3.0
Binaries:
  Node: 22.21.1
  npm: 11.7.0
Relevant Packages:
  next: 16.2.3
  react: 19.2.5
  react-dom: 19.2.5
  typescript: 5.9.3
Next.js Config:
  output: N/A

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

Instrumentation, Turbopack

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

next dev (local)

Additional context

The bug is in router-server.ts. originalFetch is captured before ensureInstrumentationRegistered() runs, so it holds the raw, un-instrumented fetch:

Timeline on cold start:
1. router-server.ts → originalFetch = globalThis.fetch   (raw fetch)
2. ensureInstrumentationRegistered() → registerOTel() → globalThis.fetch = otelWrapper
3. patchFetch() → wraps otelWrapper with Next.js cache layer
   Chain: user code → Next.js → OTel → real fetch  ✅

On every HMR event, resetFetch() restores globalThis.fetch = originalFetch — the pre-OTel raw fetch. Then patchFetch() re-wraps, but now wraps the raw fetch, permanently losing the OTel layer:

After HMR:
4. resetFetch() → globalThis.fetch = originalFetch  (pre-OTel)
5. patchFetch() → wraps raw fetch
   Chain: user code → Next.js → real fetch  ❌  (OTel wrapper lost)

Regression introduced in 15aeb92efb"misc: tweak fetch patch restoration timing during HMR to allow for userland fetch patching (#68193)" (Aug 2024). This added originalFetch capture and resetFetch().

Made worse by 0f867bb219"Turbopack: Enable server HMR by default for app pages (#91476)" (Mar 2026), which made App Router Turbopack users hit the resetFetch() path by default.

Tested on: Next.js 15.5.15, 16.0.11, 16.1.7, 16.2.3 — all affected.

Suggested fix: Re-capture originalFetch after ensureInstrumentationRegistered() completes, or have resetFetch() only strip the Next.js patchFetch layer while preserving userland wrappers.

extent analysis

TL;DR

Re-capture originalFetch after ensureInstrumentationRegistered() completes to preserve the OTel wrapper.

Guidance

  • Identify the point where ensureInstrumentationRegistered() is called and re-capture originalFetch immediately after to ensure it includes the OTel instrumentation.
  • Modify resetFetch() to strip only the Next.js patchFetch layer, preserving any userland wrappers like the OTel layer.
  • Verify the fix by triggering HMR and checking if the traceparent header is still present in the downstream terminal.
  • Consider adding logging or debugging statements to track the state of globalThis.fetch and originalFetch throughout the process to ensure the OTel wrapper is correctly applied and preserved.

Example

// After ensureInstrumentationRegistered() is called
originalFetch = globalThis.fetch; // Re-capture originalFetch with OTel wrapper

Notes

The provided fix assumes that re-capturing originalFetch after ensureInstrumentationRegistered() completes will correctly preserve the OTel wrapper. However, the actual implementation may vary depending on the specifics of the ensureInstrumentationRegistered() function and the OTel instrumentation.

Recommendation

Apply the suggested fix by re-capturing originalFetch after ensureInstrumentationRegistered() completes, as this approach directly addresses the identified issue with the timing of originalFetch capture and should preserve the OTel wrapper through HMR events.

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