headers()

Reads the incoming HTTP request headers as a read-only Web Headers object on the server.

Signature
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 Response with headers, or set them from a proxy.
  • It is async since 15. Forgetting the await gives you a Promise whose .get is 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

Example · typescript
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.

Example · typescript
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.

Example · typescript
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.

Example · typescript
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

  • Be the first to comment on this lesson.