Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 32 additions & 9 deletions packages/next/src/server/web/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,9 @@ export async function adapter(
})
let response
let cookiesFromResponse
let middlewareTraceContext: ReturnType<
ReturnType<typeof getTracer>['getTracePropagationData']
> = []

response = await propagator(request, () => {
// we only care to make async storage available for middleware
Expand Down Expand Up @@ -306,14 +309,20 @@ export async function adapter(
previouslyRevalidatedTags: [],
})

return await workAsyncStorage.run(workStore, () =>
const result = await workAsyncStorage.run(workStore, () =>
workUnitAsyncStorage.run(
requestStore,
params.handler,
request,
event
)
)

// Capture trace context while middleware span is still active
// This ensures the server span becomes a child of the middleware span
middlewareTraceContext = getTracer().getTracePropagationData()

return result
} finally {
// middleware cannot stream, so we can consider the response closed
// as soon as the handler returns.
Expand Down Expand Up @@ -469,23 +478,37 @@ export async function adapter(

const finalResponse = response ? response : NextResponse.next()

// Flight headers are not overridable / removable so they are applied at the end.
const overwrittenHeaders: string[] = []

if (middlewareTraceContext.length > 0) {
// Inject trace context headers (traceparent, tracestate) so the Node.js server
// can continue the trace started by middleware
for (const { key, value } of middlewareTraceContext) {
finalResponse.headers.set(`x-middleware-request-${key}`, value)
if (!overwrittenHeaders.includes(key)) {
overwrittenHeaders.push(key)
}
}
}

// Get existing override headers list
const middlewareOverrideHeaders = finalResponse.headers.get(
'x-middleware-override-headers'
)
const overwrittenHeaders: string[] = []
// Flight headers are not overridable / removable so they are applied at the end.
if (middlewareOverrideHeaders) {
for (const [key, value] of flightHeaders) {
finalResponse.headers.set(`x-middleware-request-${key}`, value)
overwrittenHeaders.push(key)
}
}

if (overwrittenHeaders.length > 0) {
finalResponse.headers.set(
'x-middleware-override-headers',
middlewareOverrideHeaders + ',' + overwrittenHeaders.join(',')
)
}
// Update the override headers list if we added any headers
if (overwrittenHeaders.length > 0) {
finalResponse.headers.set(
'x-middleware-override-headers',
middlewareOverrideHeaders + ',' + overwrittenHeaders.join(',')
)
Comment on lines +508 to +511
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
finalResponse.headers.set(
'x-middleware-override-headers',
middlewareOverrideHeaders + ',' + overwrittenHeaders.join(',')
)
const headerValue = middlewareOverrideHeaders
? middlewareOverrideHeaders + ',' + overwrittenHeaders.join(',')
: overwrittenHeaders.join(',')
finalResponse.headers.set('x-middleware-override-headers', headerValue)

When setting the x-middleware-override-headers header at line 510, the code concatenates middlewareOverrideHeaders (which can be null) with a string, resulting in the literal string "null" being prepended to the header value. This corrupts the header when trace context is captured but no previous override headers exist.

View Details

Analysis

Null concatenation in x-middleware-override-headers header when trace context is captured without existing overrides

What fails: In packages/next/src/server/web/adapter.ts lines 507-512, when middleware captures trace context but no previous override headers exist, the code concatenates null with string values, resulting in the literal string "null" being prepended to the header value.

How to reproduce:

  1. Middleware runs and captures trace context (triggers middlewareTraceContext.length > 0)
  2. No previous middleware override headers exist (middlewareOverrideHeaders is null from finalResponse.headers.get())
  3. Line 510 executes: middlewareOverrideHeaders + ',' + overwrittenHeaders.join(',')
  4. Result: null + ',' + 'traceparent,tracestate' = "null,traceparent,tracestate"

Result: The x-middleware-override-headers header contains the literal string "null" as the first element, which corrupts header propagation and may break trace propagation in the server-side code.

Expected: When middlewareOverrideHeaders is null, the header should be set to only the overwritten headers list without the "null" prefix, i.e., "traceparent,tracestate".

Root cause: The refactoring in commit a711b1c ("propagate context in middleware response headers") moved the header update outside the if (middlewareOverrideHeaders) conditional block, but did not add a null check when concatenating the values.

}

return {
Expand Down
33 changes: 16 additions & 17 deletions test/e2e/opentelemetry/instrumentation/opentelemetry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -463,23 +463,22 @@ describe('opentelemetry', () => {
'next.span_type': 'Middleware.execute',
},
status: { code: 0 },
spans: [],
},

{
runtime: 'nodejs',
traceId: env.span.traceId,
parentId: env.span.rootParentId,
name: 'GET /behind-middleware',
attributes: {
'http.method': 'GET',
'http.route': '/behind-middleware',
'http.status_code': 200,
'http.target': '/behind-middleware',
'next.route': '/behind-middleware',
'next.span_name': 'GET /behind-middleware',
'next.span_type': 'BaseServer.handleRequest',
},
spans: [
{
runtime: 'nodejs',
traceId: env.span.traceId,
name: 'GET /behind-middleware',
attributes: {
'http.method': 'GET',
'http.route': '/behind-middleware',
'http.status_code': 200,
'http.target': '/behind-middleware',
'next.route': '/behind-middleware',
'next.span_name': 'GET /behind-middleware',
'next.span_type': 'BaseServer.handleRequest',
},
},
],
},
])
})
Expand Down
Loading