headers()
Reads the incoming HTTP request headers as a read-only Web Headers object on the server.
function headers(): Promise<ReadonlyHeaders>- Added in Next.js v13.0.0
Return value
Promise<ReadonlyHeaders>A promise resolving to a read-only Web Headers object supporting entries(), forEach(), get(), has(), keys() and values().
Errors
Attempting to set() or delete() on the returned object throws, because the incoming headers are read-only.
Changelog
- PHP v15.0.0-RCheaders is now an async function and must be awaited. A codemod is available; synchronous access still works in 15 but is deprecated.
- PHP v13.0.0headers introduced.
headers() hands you the request headers the browser or upstream proxy sent, as a standard Web Headers instance with the mutating methods removed. Everything you know from fetch works: get(), has(), iteration with entries().
When to reach for it
Reach for it when the thing you need is on the request but not in a cookie: an authorization bearer token to forward to your API, accept-language for locale negotiation, x-forwarded-for for geo or rate limiting, or a header your proxy injected upstream via NextResponse.next({ request: { headers } }).
What trips people up
- It is read-only in both directions. You cannot set a response header with it — return a
Responsewith headers, or set them from a proxy. - It is async since 15. Forgetting the
awaitgives you a Promise whose.getis undefined. - It is a request-time API. Any route that reads it renders dynamically, and under Cache Components it must sit inside a
<Suspense>boundary or the route cannot be prerendered. - Header names are case-insensitive, but a missing header returns
null, not an empty string.
Example
import { headers } from 'next/headers'
export default async function Page() {
const headersList = await headers()
const userAgent = headersList.get('user-agent') ?? 'unknown'
const acceptLanguage = headersList.get('accept-language') ?? 'en'
return (
<main>
<p>User agent: {userAgent}</p>
<p>Preferred language: {acceptLanguage}</p>
</main>
)
}When to use it
- Forward an incoming Authorization header to an upstream API from a Server Component
- Pick a locale from accept-language before rendering
- Rate-limit by the client IP in x-forwarded-for
- Read a header your proxy injected upstream for the page to consume
Frequently asked questions
How do I get the client IP address in Next.js?
Read the x-forwarded-for header — const ip = (await headers()).get('x-forwarded-for') — and take the first comma-separated entry. NextRequest no longer exposes request.ip; it was removed in v15.0.0, so the header is the supported route on every host.
Can I set a response header with headers()?
No. The object returned is read-only and represents the incoming request only. Set response headers by returning a Response with a headers option from a Route Handler, or from proxy.js with NextResponse.
Why is headers() making my whole page dynamic?
Because its value cannot be known before a request arrives, so reading it in a layout or page opts the route into dynamic rendering. Push the call down into a small child component wrapped in <Suspense> so the rest of the route still prerenders.
More examples
Forwarding the Authorization header
The page acts as a pass-through for the caller's credential. Because the page calls headers(), the route already renders dynamically and the unset 'auto no cache' default would fetch on every request anyway; cache: 'no-store' states that intent explicitly and guarantees the response is never written to the data cache. The null check matters for a different reason: headersList.get() returns string | null, and passing a null value through would send a literal 'null' authorization header upstream.
import { headers } from 'next/headers'
export default async function Page() {
const authorization = (await headers()).get('authorization')
const res = await fetch('https://api.example.com/me', {
headers: authorization ? { authorization } : {},
cache: 'no-store',
})
const user = await res.json()
return <h1>{user.name}</h1>
}Keeping the static shell with Suspense
Only LocaleBanner reads the request, so only it is excluded from the prerender. The <h1> and the fallback ship in the static shell and appear instantly; the banner streams in once the request is known. Calling headers() directly in Page instead would make the whole route dynamic.
import { Suspense } from 'react'
import { headers } from 'next/headers'
export default function Page() {
return (
<main>
<h1>Docs</h1>
<Suspense fallback={<p>Detecting language…</p>}>
<LocaleBanner />
</Suspense>
</main>
)
}
async function LocaleBanner() {
const acceptLanguage = (await headers()).get('accept-language') ?? 'en'
const locale = acceptLanguage.split(',')[0]
return <p>Serving content for {locale}</p>
}Iterating every header in a Route Handler
entries() yields lower-cased header names in the order the runtime holds them, so the sort makes the JSON output stable between requests. Useful when debugging what a CDN or load balancer is actually adding in front of your app.
import { headers } from 'next/headers'
export async function GET() {
const headersList = await headers()
const received = Object.fromEntries(
[...headersList.entries()].sort(([a], [b]) => a.localeCompare(b))
)
return Response.json({ received })
}
Discussion