Skip to content

Route Attributes are not propagated to the handleRequest span when nextjs is used with a custom server #85520

@eli0shin

Description

@eli0shin

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions