Setting Request and Response Headers in Proxy

The proxy file can add headers in two directions: forward to your server code, or back to the browser. They use different APIs, and mixing them up leaks data or breaks responses.

8 min read

Setting request and response headers in proxy covers two different jobs that share one word. A request header travels forward to the code that renders the route, while a response header travels back to the browser.

Next.js keeps these separate on purpose, and the API makes the choice explicit. Getting them confused is how private values end up in a browser response.

typescripttypescript
// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function proxy(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', 'proxy')
  return response
}

The request id reaches your page and never appears in the browser. The served-by header is sent to the browser, where it shows up in the network panel. Both were set in the same function, in opposite directions.

Request headers go upstream

To add a header your own server code can read, clone the incoming headers, set what you need, and pass the result through the request option.

Cloning matters. The incoming headers object is not meant to be mutated in place, so build a new one from it and modify that.

typescripttypescript
// proxy.ts
export function proxy(request: NextRequest) {
  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-tenant', request.nextUrl.pathname.split('/')[1] ?? '')
  return NextResponse.next({ request: { headers: requestHeaders } })
}

Every route under a tenant segment now receives a header naming that tenant, without each page having to parse the pathname itself. The value is added to the request on its way in, so nothing about the response changes.

Reading it back uses the standard request API. In a Server Component, await the headers function and ask for the key.

App.tsxApp.tsx
// app/[tenant]/page.tsx
import { headers } from 'next/headers'
 
export default async function Page() {
  const tenant = (await headers()).get('x-tenant')
  return <h1>Workspace: {tenant}</h1>
}

The heading renders with the value the proxy file computed. Because this component reads request data, the route renders at request time rather than being prerendered, which is worth knowing before you add a forwarded header to a page you wanted static.

Response headers go to the browser

When the browser is the audience, set the header on the response object you return. That covers security headers, cache directives, and anything a client or CDN needs to act on.

typescripttypescript
// proxy.ts
export function proxy(request: NextRequest) {
  const response = NextResponse.next()
  response.headers.set('x-frame-options', 'SAMEORIGIN')
  return response
}

Requesting any matched route now returns that header, visible in the network panel alongside the response.

The direction is not symmetric, and it is worth knowing which way each one leaks. A header forwarded through the request option stays upstream and never reaches the browser, while a header set on the response object is sent to the browser and is also readable by your page.

That asymmetry decides where a private value can go. Anything the browser should not see belongs in the request option, because the response object is not a private channel.

Which paths get the header is the matcher's job, not the function's. A proxy file with no matcher adds these headers to stylesheets, scripts, and images too, so scope the matcher before adding anything to every response.

The shorthand that causes trouble

There is a second shape that looks almost identical and behaves completely differently. Passing a headers option directly to the next call sends those headers to the client.

typescripttypescript
// proxy.ts
export function proxy(request: NextRequest) {
  const headers = new Headers(request.headers)
  // Sends every incoming header back to the browser
  return NextResponse.next({ headers })
}

This is not a way to forward headers upstream. Your page reads nothing from it, while the browser receives every key. Requesting a matched route with a session cookie and a bearer token shows exactly what comes back.

texttext
HTTP/1.1 200 OK
authorization: Bearer topsecret
cookie: session=secret-value
content-type: application/json

Those two values were the client's to begin with, so nothing new leaks to that visitor, but they are now written into a response that shared caches and proxies can store. Note what is also missing: the HTML content type the page needed has been replaced by the one the request happened to carry.

It can also break responses

Response headers set this way can override what the framework expects. A Content-Type carried over from the request will conflict with the content type Server Actions and streaming responses rely on, producing failed submissions rather than an obvious error.

The rule is short. If the option is nested under a request key, it goes upstream. If it is not, it goes to the browser.

Static headers belong in the config file

A proxy file is the wrong home for headers that never change. The Next.js config has a headers function for exactly that, and it runs before the proxy file and before filesystem routes.

typescripttypescript
// next.config.ts
async headers() {
  return [
    {
      source: '/:path*',
      headers: [{ key: 'x-content-type-options', value: 'nosniff' }],
    },
  ]
}

Every matched response carries the header, with no function invoked per request. This is the right default for the standard security header set, and it keeps the proxy file free for decisions that genuinely need the request.

Move a header into the proxy file only when its value varies. A content security policy nonce is the clearest example, since it must be unique per request and must be readable both by the browser and by the rendering code, which is one of the few jobs the proxy file is genuinely the right place for.

Note that a nonce forces request-time rendering for any page that uses it, because a value generated per request cannot be baked into a prerendered page.

What Proxy does not see

Next.js strips its internal navigation headers from the request instance before your function sees it. Values such as the flight request marker and the router state tree are not available through the request headers.

This is deliberate. An RSC request and the HTML request for the same route must be handled the same way, and reading those headers is the easiest way to accidentally treat them differently.

Prefetch requests are the exception you can act on, through the matcher rather than the function body. A matcher entry can skip requests carrying a prefetch header, which keeps expensive header work off navigations the visitor has not committed to.

Safety rules worth following

Headers are cheap to add and easy to leak, so a few habits are worth making automatic.

  • Never copy the whole incoming header set onto a response. Build an allow-list of the specific keys you need.
  • Keep authorization values and cookies out of anything traveling to the browser or to an external service.
  • Keep header values small, since oversized headers can produce a 431 response depending on the server in front of your app.
  • Treat a forwarded header as untrusted input if any part of it came from the client.

That last point catches people. A header the proxy file computed from the pathname is trustworthy, but one it copied from the request is exactly as trustworthy as the request was, which is not at all. Securing Server Actions covers validating that kind of input where it is used.

Rune AI

Rune AI

Key Insights

  • Request headers set through the request option go upstream and are not exposed to the browser.
  • Response headers set on the returned response object reach the client, and your page can read them too.
  • Passing headers directly into next() sends them to the client and can override framework expectations.
  • Read forwarded headers in a Server Component by awaiting the headers function from next/headers.
  • Static response headers belong in the config headers function, not in the proxy file.
RunePowered by Rune AI

Frequently Asked Questions

How do I read a header the proxy file added?

In a Server Component or Route Handler, await the headers function from next/headers and call get with the header name. The value is only there if the proxy forwarded it through the request option rather than setting it on the response.

What is wrong with passing headers directly to next()?

That shorthand sends them to the browser instead of upstream, so your page reads nothing from it. Beyond exposing values you may not have meant to expose, setting response headers such as Content-Type can override what the framework expects and break Server Action submissions or streaming.

Why can I not see the RSC headers in proxy?

Next.js strips internal Flight headers such as rsc and next-router-state-tree from the request instance in proxy. It prevents an RSC request being handled differently from the HTML request for the same route, since the two need to agree.

Should security headers go in proxy or the config file?

Put static security headers in the config headers function, since they need no request data and are applied before the proxy file runs. Use proxy only when the value changes per request, such as a CSP nonce.

Conclusion

Proxy can set headers in two directions, and the API you choose decides who sees them. Forward values upstream with the request option so your server code can read them, set them on the response object when the browser needs them, and keep static security headers in the config file where they are cheaper and easier to audit.