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.
// 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.
// 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/[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.
// 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.
// 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.
HTTP/1.1 200 OK
authorization: Bearer topsecret
cookie: session=secret-value
content-type: application/jsonThose 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.
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.
// 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
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.
Frequently Asked Questions
How do I read a header the proxy file added?
What is wrong with passing headers directly to next()?
Why can I not see the RSC headers in proxy?
Should security headers go in proxy or the config file?
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.
More in this topic
`generateMetadata` Explained with Real Examples
What generateMetadata does, when it runs, and how to use it for real routes: awaited params, deduplicated data fetching, extending parent metadata, and returning a 404 from metadata.
Canonical URLs in Next.js: `metadataBase`, `alternates.canonical`, and Dynamic Pages
How canonical URLs work in the Next.js App Router: setting metadataBase once, writing alternates.canonical per route, handling dynamic segments, and what happens when the base URL is missing.
Open Graph and Twitter Card Metadata in Next.js
How to write Open Graph and Twitter card metadata in the Next.js App Router: the openGraph and twitter fields, automatic card defaults, article tags, and image merge rules.