nextjs - ✅(Solved) Fix Route Attributes are not propagated to the handleRequest span when nextjs is used with a custom server [1 pull requests, 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#85520Fetched 2026-04-08 02:15:14
View on GitHub
Comments
1
Participants
2
Timeline
11
Reactions
1
Author
Timeline (top)
referenced ×4labeled ×2closed ×1commented ×1

Root Cause

Next.js conflates "root span" (no parent) with "span where we should store propagated attributes". With a custom server:

  • Next.js's handleRequest span has a parent (the custom server's span)
  • So it's not treated as a root span (isRootSpan = false)
  • But it still needs the attribute store initialized to collect route information from child spans
  • The attributes should be going to Next.js's handleRequest span, but the propagation mechanism is disabled because that span isn't flagged as a root span

Fix Action

Fixed

PR fix notes

PR #85521: fix: support root span attributes with a custom server

Description (problem / solution / changelog)

<!-- Thanks for opening a PR! Your contribution is much appreciated. To make sure your PR is handled as smoothly as possible we request that you follow the checklist sections below. Choose the right checklist for the change(s) that you're making: ## For Contributors ### Improving Documentation - Run `pnpm prettier-fix` to fix formatting issues before opening the PR. - Read the Docs Contribution Guide to ensure your contribution follows the docs guidelines: https://nextjs.org/docs/community/contribution-guide ### Fixing a bug - Related issues linked using `fixes #number` - Tests added. See: https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs - Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md ### Adding a feature - Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. (A discussion must be opened, see https://github.com/vercel/next.js/discussions/new?category=ideas) - Related issues/discussions are linked using `fixes #number` - e2e tests added (https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) - Documentation added - Telemetry added. In case of a feature if it's used or not. - Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md ## For Maintainers - Minimal description (aim for explaining to someone not on the team to understand the PR) - When linking to a Slack thread, you might want to share details of the conclusion - Link both the Linear (Fixes NEXT-xxx) and the GitHub issues - Add review comments if necessary to explain to the reviewer the logic behind a change ### What? ### Why? ### How? Closes NEXT- Fixes # -->

Fixes #85520

When there is an active context from a custom server, the handleRequest span is not marked as a root span. As a result next.js does not track any root span attributes on the handleRequest span. Because the custom server does not know what route was matched by next and next is not tracking the route that matched this makes observability very challenging for users of a custom server.

This PR changes the isRootSpan logic to treat the handleRequest span as a root span if there is not already a nextjs root span for the request instead of whether there is any active span.

Changed files

  • packages/next/src/server/lib/trace/tracer.ts (modified, +8/-4)
  • test/e2e/opentelemetry/instrumentation/custom-server.ts (added, +49/-0)
  • test/e2e/opentelemetry/instrumentation/instrumentation-custom-server.ts (added, +107/-0)
  • test/e2e/opentelemetry/instrumentation/opentelemetry.test.ts (modified, +178/-1)
  • test/e2e/opentelemetry/instrumentation/package.json (modified, +2/-0)

Code Example

This behavior is entirely independent of the environment.

---

return tracer.withPropagatedContext(req.headers, () => {
  return tracer.trace(BaseServerSpan.handleRequest, {...}, async (span) => {
    // This span IS created, but...

---

if (isRootSpan) {
  rootSpanAttributesStore.set(spanId, new Map(...))
}

---

let spanContext = this.getSpanContext(
  options?.parentSpan ?? this.getActiveScopeSpan()
)
let isRootSpan = false

if (!spanContext) {
  spanContext = context?.active() ?? ROOT_CONTEXT
  isRootSpan = true
} else if (trace.getSpanContext(spanContext)?.isRemote) {
  isRootSpan = true
}

---

public setRootSpanAttribute(key: AttributeNames, value: AttributeValue) {
  const spanId = context.active().getValue(rootSpanIdKey) as number  // undefined!
  const attributes = rootSpanAttributesStore.get(spanId)  // store has no entry
  if (attributes && !attributes.has(key)) {
    attributes.set(key, value)
  }
}

---

const rootSpanAttributes = tracer.getRootSpanAttributes()
if (!rootSpanAttributes) return // Returns early, route never gets set

---

public getRootSpanAttributes() {
  const spanId = context.active().getValue(rootSpanIdKey) as number  // undefined!
  return rootSpanAttributesStore.get(spanId)  // returns undefined
}
RAW_BUFFERClick to expand / collapse

Link to the code that reproduces this issue

https://github.com/eli0shin/next-js-custom-server-repro

To Reproduce

  1. build the server
  2. run npm run start to run nextjs with the custom server
  3. open the page in your browser
  4. the otel spans will be logged in the console. the handleRequest span is missing the http.route and next.route attributes
  5. stop the server
  6. start the regular server with npm run start:regular
  7. open the page in your browser
  8. the spans are logged to the console. the http.route and next.route attributes are on the handleRequest span

Current vs. Expected behavior

Current

When a custom server is used no span contains the http.route attribute for the matched nextjs route

Expected

When a custom server is used the handleRequest route contains the http.route attribute for the matched route

Provide environment information

This behavior is entirely independent of the environment.

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

Instrumentation

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

Other (Deployed)

Additional context

When Next.js is used with a custom server that creates its own root span, Next.js still creates its own handleRequest span, but it fails to propagate attributes (like next.route and http.route) to the span that it creates because the attribute propagation mechanism requires isRootSpan = true. The impact of this issue is that no server span in the trace contains the http.route attribute breaking trace visibility and trace derived metrics. Next.js is the router in this case and there is no way to set the matched route on either the next.js server span or the custom server's server span.

The Actual Issue

Next.js creates the handleRequest span at packages/next/src/server/base-server.ts:869-871:

return tracer.withPropagatedContext(req.headers, () => {
  return tracer.trace(BaseServerSpan.handleRequest, {...}, async (span) => {
    // This span IS created, but...

However, the rootSpanAttributesStore is only initialized when isRootSpan = true at packages/next/src/server/lib/trace/tracer.ts:337-347:

if (isRootSpan) {
  rootSpanAttributesStore.set(spanId, new Map(...))
}

With a custom server parent span:

  • isRootSpan = false (because there's a parent context)
  • No entry is added to rootSpanAttributesStore for this spanId
  • The spanId is never stored in the context via rootSpanIdKey

Root Span Detection Logic

packages/next/src/server/lib/trace/tracer.ts:284-295:

let spanContext = this.getSpanContext(
  options?.parentSpan ?? this.getActiveScopeSpan()
)
let isRootSpan = false

if (!spanContext) {
  spanContext = context?.active() ?? ROOT_CONTEXT
  isRootSpan = true
} else if (trace.getSpanContext(spanContext)?.isRemote) {
  isRootSpan = true
}

When a custom server propagates its span:

  1. Next.js extracts the remote context via withPropagatedContext() at packages/next/src/server/base-server.ts:867
  2. spanContext is NOT falsy because there's a parent span from the custom server
  3. The isRemote check may not properly identify the extracted context, OR the span is not marked as remote
  4. isRootSpan remains false

Why Attribute Propagation Fails

When child spans call setRootSpanAttribute('next.route', pathname) at these locations:

  • packages/next/src/server/base-server.ts:2458
  • packages/next/src/server/render.tsx:1427
  • packages/next/src/server/app-render/app-render.tsx:1710
  • packages/next/src/server/route-modules/app-route/module.ts:781
  • packages/next/src/server/api-utils/index.ts:77

They try to retrieve the root span ID from context at packages/next/src/server/lib/trace/tracer.ts:456-462:

public setRootSpanAttribute(key: AttributeNames, value: AttributeValue) {
  const spanId = context.active().getValue(rootSpanIdKey) as number  // undefined!
  const attributes = rootSpanAttributesStore.get(spanId)  // store has no entry
  if (attributes && !attributes.has(key)) {
    attributes.set(key, value)
  }
}

Since rootSpanIdKey was never set in the context (only happens at packages/next/src/server/lib/trace/tracer.ts:305 when isRootSpan = true), and the store has no entry for this span, the attributes can't be propagated to the Next.js span.

Failed Attribute Retrieval

When the request completes, packages/next/src/server/base-server.ts:898-899 tries to retrieve the attributes:

const rootSpanAttributes = tracer.getRootSpanAttributes()
if (!rootSpanAttributes) return // Returns early, route never gets set

getRootSpanAttributes() at packages/next/src/server/lib/trace/tracer.ts:451-454:

public getRootSpanAttributes() {
  const spanId = context.active().getValue(rootSpanIdKey) as number  // undefined!
  return rootSpanAttributesStore.get(spanId)  // returns undefined
}

Root Cause

Next.js conflates "root span" (no parent) with "span where we should store propagated attributes". With a custom server:

  • Next.js's handleRequest span has a parent (the custom server's span)
  • So it's not treated as a root span (isRootSpan = false)
  • But it still needs the attribute store initialized to collect route information from child spans
  • The attributes should be going to Next.js's handleRequest span, but the propagation mechanism is disabled because that span isn't flagged as a root span

Impact

The Next.js handleRequest span is created but lacks critical attributes:

  • next.route is not set
  • http.route is not set
  • Span name remains generic (e.g., GET instead of GET /api/users)
  • Observability is significantly degraded and breaks trace metrics in providers like Datadog

extent analysis

TL;DR

The most likely fix is to modify the Next.js tracer to initialize the rootSpanAttributesStore even when isRootSpan is false, allowing attribute propagation to the handleRequest span.

Guidance

  1. Review the isRootSpan detection logic: Ensure that the logic correctly identifies the root span, even when a custom server is used.
  2. Initialize rootSpanAttributesStore for non-root spans: Modify the tracer.ts file to initialize the rootSpanAttributesStore for the handleRequest span, even when isRootSpan is false.
  3. Update attribute propagation: Verify that the attribute propagation mechanism is updated to store attributes in the rootSpanAttributesStore for the handleRequest span.
  4. Test with custom server: Test the changes with a custom server to ensure that the handleRequest span now contains the expected attributes (next.route and http.route).

Example

// packages/next/src/server/lib/trace/tracer.ts
if (isRootSpan || !spanContext) {
  rootSpanAttributesStore.set(spanId, new Map(...))
}

This example initializes the rootSpanAttributesStore for the handleRequest span, even when isRootSpan is false.

Notes

The provided solution assumes that the issue is caused by the isRootSpan detection logic and the initialization of the rootSpanAttributesStore. Further investigation may be required to ensure that the solution works as expected in all scenarios.

Recommendation

Apply the workaround by modifying the tracer.ts file to initialize the rootSpanAttributesStore for non-root spans, as described in the guidance section. This should allow attribute propagation to the handleRequest span and resolve the issue.

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