Reading
Anywhere in a server component or action:
import { headers, cookies, searchParams } from '@rsc-kit/core/request'
export default async function Page() {
const h = await headers()
const jar = await cookies()
return <p>Hello {jar.get('name')?.value ?? 'stranger'}</p>
}Writing
import { responseHeaders, cookies } from '@rsc-kit/core/request'
export async function middleware() {
responseHeaders().set('X-Frame-Options', 'DENY')
const jar = await cookies()
jar.set('last-seen', new Date().toISOString(), { httpOnly: true, path: '/' })
}Writing only works in middleware. Middleware runs before the render, while the response line has not been sent yet. A component runs during streaming, when the headers are already on the wire — writing from one throws rather than being silently dropped, so you find out immediately.
A redirect carries them too, which is what lets middleware remember where someone was going before sending them to log in:
export async function middleware() {
const jar = await cookies()
if (!jar.get('session')) {
jar.set('intended', '/dashboard', { path: '/' })
redirect('/login')
}
}Security headers
Nothing sets them for you, because which ones an app wants is the app’s decision. The ones that are the same on every page go in the config:
rscKit({
headers: {
'X-Content-Type-Options': 'nosniff',
'Referrer-Policy': 'strict-origin-when-cross-origin',
'X-Frame-Options': 'DENY',
'Strict-Transport-Security': 'max-age=63072000; includeSubDomains',
},
})Every response gets them, and a page stays one an edge may serve from its cache: they are fixed for the build, so the edge sends them with its copy. A middleware’s own value for the same header wins.
Only the headers an edge can send with a cached page are accepted:
Content-Language, X-Robots-Tag, Content-Security-Policy,
Permissions-Policy, Referrer-Policy, Strict-Transport-Security,
X-Frame-Options, X-Content-Type-Options, Cross-Origin-Opener-Policy,
Cross-Origin-Embedder-Policy, Cross-Origin-Resource-Policy and
X-DNS-Prefetch-Control. Anything else is a build error that says so.
Cache-Control and Link are left out because the renderer sets them per
response.
A header that depends on the request goes in a middleware instead. A
middleware runs per request, so a page under one is never served from an
edge’s cache: a root middleware.ts keeps every page out.
A Content-Security-Policy is the one that needs more than a header: a
page stored at build time cannot carry a per-request nonce. That is
on the list, and until then a
policy that allows your own origin’s scripts is the honest one.
Cookie options
jar.set('name', 'value', {
httpOnly: true,
secure: true,
sameSite: 'lax', // 'strict' | 'lax' | 'none'
path: '/',
maxAge: 60 * 60 * 24,
expires: new Date('2027-01-01'),
})Names are validated as cookie tokens and sameSite / expires are checked, so
a typo is an error rather than a header the browser quietly ignores.
A cookie set is the cookie read, for the rest of the request. After
jar.set('agent', id), every cookies().get('agent') in the same request —
in the action, and in the sections it revalidate()s, which render in the
same request and travel back with the answer — sees id, not what the
browser sent; a deleted one is gone from get, has and getAll. This is
Next’s rule too, and it is what makes set then revalidate('all') the
whole of a switcher: the sidebar re-renders with the choice the browser is
about to hold. The next request starts from what it arrives with.
What a response says about itself
Two things, and they are not the same kind:
How it was served, always: X-RSC-Kit: stored, rendered or shell on
every response — a page from a file the build wrote, a page rendered for this
visitor, or a stored shell with its holes rendered now. It is the header a
developer reads in the Network tab when a page is slower than expected, the
way X-Nextjs-Cache is, and a CDN rule or a health check can key on it. It
names no product, so there is nothing to strip.
What built it, by default: X-Powered-By: rsc-kit on every response and
<meta name="generator" content="rsc-kit"> in every document — what
BuiltWith and Wappalyzer read. The name only, never the version: a version in
every response is what a vulnerability scanner filters on. A policy that
strips every framework identifier turns both off, which matters for the tag,
since a proxy can strip a header but not a line inside the HTML:
rscKit({ identify: false })