skip to content

In Next.js middleware you compute a value — say a request ID — that the Server Component handling the same request needs. How do you pass it down, and why doesn't setting it on request.headers work?

level: middleimportance: must knowfreq 55%

answer

  1. two separate pieces of server code
  2. the request is the only channel inward
  3. clone, don't mutate
  4. request headers in, response headers out
  5. await headers() on the other side

basics

~20 s

Clone the incoming headers, set your value on the clone, and pass it through NextResponse.next({ request: { headers: cloned } }). The route then reads it with await headers(). The incoming request.headers object is not the copy Next forwards inward, so mutating it changes nothing.

solid answer

~40 s

Middleware and the route that renders are two separate pieces of server code; the only channel between them is the request Next forwards inward. To use it, build `const requestHeaders = new Headers(request.headers)`, set your value on that copy, and return `NextResponse.next({ request: { headers: requestHeaders } })`. Downstream, a Server Component reads it with `await headers()` from `next/headers`, and a Route Handler reads `request.headers.get(...)`. Mutating `request.headers` in place is the usual wrong attempt — that object represents what arrived, and Next only forwards headers you hand it through the `request` option of `next()`. The other classic confusion is direction: `response.headers.set(...)` on the object you return goes *outward* to the browser and is never visible to the route. Rewrites accept the same `request` option; a redirect does not, because the request ends there.

code

typescript · 14 lines
typescript
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-request-id', crypto.randomUUID())

  const response = NextResponse.next({
    request: { headers: requestHeaders },
  })

  response.headers.set('x-served-by', 'middleware')
  return response
}

go deeper

for a junior

Remember the shape of the call: copy the incoming headers into a new Headers object, set your value, and pass it as NextResponse.next({ request: { headers } }).

for a middle

Explain why in-place mutation of request.headers does nothing, and be precise that request headers travel inward to the route while response headers travel outward to the browser.

for a senior

Show that you treat forwarded headers as a trust boundary — always replace rather than conditionally set — and that reading headers() marks that segment as request-dependent rather than prerenderable.

for a principal

Own the convention: which header names are reserved for platform use, who is allowed to write them, and what guarantees hold on paths where middleware does not run, so no route ever trusts a value that a client could have supplied.

## Two pieces of server code, one channel It is tempting to think of middleware and the page as one continuous piece of server work sharing variables. They are not. Middleware runs first, decides what happens to the request, and hands control on. There is no shared module scope you can rely on, no request-scoped context object you can stash things in. The one channel Next gives you is the **request that gets forwarded inward**, and specifically its headers. ## The mechanism ```ts import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' export function middleware(request: NextRequest) { const requestHeaders = new Headers(request.headers) requestHeaders.set('x-request-id', crypto.randomUUID()) return NextResponse.next({ request: { headers: requestHeaders }, }) } ``` Three things are load-bearing: 1. **`new Headers(request.headers)`** — you copy, because the incoming headers are what arrived, not the outgoing set. 2. **the `request` option on `next()`** — this is the whole API. Without it, `next()` forwards the request unchanged. 3. **a header, not an object** — headers carry strings. Anything structured has to be serialised, and it has to be small; every proxy in the path has header size limits, and the value travels on the request whether the route uses it or not. `NextResponse.rewrite(destination, { request: { headers: requestHeaders } })` accepts the same option, so you can rewrite and forward headers in one return. ## Reading it downstream In a Server Component or a layout, use the `headers()` helper: ```ts import { headers } from 'next/headers' export default async function Page() { const requestId = (await headers()).get('x-request-id') return <p>{requestId}</p> } ``` Since Next 15, `headers()`, `cookies()` and `draftMode()` are async and must be awaited — code written against Next 14 calls them synchronously and will need updating. In a Route Handler you already have a request object, so `request.headers.get('x-request-id')` is enough. Note what reading `headers()` implies: it is a request-time API, so the segment that calls it cannot be rendered ahead of time. Passing data down this way is not free — you are also declaring that this part of the tree depends on the incoming request. ## Why mutating `request.headers` does nothing The `NextRequest` you receive models the request as it arrived. Nothing about Next's forwarding reads back from that object after your function returns; the forwarded set is exactly what you supplied through the `request` option, and if you supplied nothing, it is the original. So `request.headers.set('x-request-id', id)` either throws (a `Headers` object can be immutable) or silently changes an object nobody reads afterwards. Either way, the route sees nothing, which is a frustrating bug precisely because it looks like it should work. ## The direction mistake The other half of the confusion is the response: ```ts const response = NextResponse.next() response.headers.set('x-request-id', id) // → goes to the BROWSER ``` That sets a header on the response Next sends back to the client. It is the right tool for `Content-Security-Policy`, a debug marker, or anything the client or a CDN should see — and it is useless for talking to your own route, which has already finished by the time that header exists. Request headers travel inward; response headers travel outward. Keep the two straight and most middleware confusion evaporates. ## What does not survive A redirect ends the request. `NextResponse.redirect()` has no request-forwarding option, because there is no inward leg left — the browser starts a fresh request, and anything you wanted to carry has to be in the URL or in a cookie you set on the redirect response. ## Practical cautions Anything you forward is attacker-controllable unless you overwrite it. A client can send `x-request-id` — or `x-user-id` — on its own request, and if your middleware only sets the header conditionally, the route cannot tell your value from theirs. Always `set` (which replaces) rather than `append`, and treat forwarded headers as trusted only because middleware unconditionally overwrites them. That discipline matters most for anything identity-shaped, where a forwarded header that the route trusts becomes a bypass if middleware ever fails to run for that path.

  • How would a Route Handler read the header instead of a Server Component?
    A Route Handler is given the request, so it reads `request.headers.get('x-request-id')` directly — no `next/headers` import needed. The `headers()` helper exists for Server Components and layouts, which receive no request object of their own.
  • Can you forward a header and rewrite in the same return?
    Yes. `NextResponse.rewrite(destination, { request: { headers: requestHeaders } })` takes the same `request` option as `next()`, so the rewritten route receives your modified headers. A redirect cannot: it terminates the request, so use the URL or a cookie set on the redirect response instead.
  • What stops a client from sending that header itself?
    Nothing — clients control their own request headers. Middleware must unconditionally `set` (replace) the value rather than only filling it in when absent, and the route should trust it only because middleware always overwrites it. Never let a forwarded header carry identity that the route accepts without its own check.

saying these in an interview costs you the question

  • Mutates request.headers and expects the route to see it
  • Sets the header on the response and wonders why the page can't read it
  • Thinks middleware and the page share request-scoped variables
  • Calls headers() without awaiting it on current Next
  • Trusts a forwarded header a client could have sent itself

context