proxy — headers

Two directions, easy to confuse. NextResponse.next({ request: { headers } }) makes headers available upstream to the render. NextResponse.next({ headers }) sends them downstream to the client.

Note also that request.headers is not writable in place — clone it into a new Headers object first.

What to check

Request side (what the render sees) is the panel below. It is a dynamic hole, because headers() is a runtime API — a cached scope cannot read it.

Response side (what the client sees) needs curl, since the page cannot read its own response headers:

curl -sI localhost:3000/proxy/headers | grep -i x-proxy

Correlate them. The x-proxy-invocation response header and the x-from-proxy-invocation request header are set to the same id in one invocation, so they should match for a given request. That is the cleanest proof both directions came from the same proxy call.

Keep them small. Large headers can trigger 431 Request Header Fields Too Large depending on the upstream server.

Injected request headers

What the render received

suspense fallback

streaming at request time — this is what ships in the static shell

Reference

headerdirectionhow to read it
x-from-proxy-invocationrequest → renderheaders() in a dynamic scope
x-from-proxy-georequest → renderheaders() in a dynamic scope
x-proxy-invocationresponse → clientcurl -sI, or devtools
x-proxy-branchresponse → clientcurl -sI, or devtools
x-proxy-prefetchresponse → clientcurl -sI with a prefetch header
x-proxy-pathresponse → clientcurl -sI (pre-rewrite pathname)