@rsc-kit/mcp
Version:
An MCP server over what an rsc-kit build decided: the routes, why each one is static or not, and what it costs the browser.
121 lines (91 loc) • 4.03 kB
Markdown
# Headers and cookies
> Setting response headers and cookies during a render.
## Reading
Anywhere in a server component or action:
```tsx
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
```tsx
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:
```tsx
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 usual set is one middleware at the root, which every route
then inherits:
```tsx title="src/app/middleware.ts"
import { responseHeaders } from '@rsc-kit/core/request'
export default function middleware() {
const h = responseHeaders()
h.set('X-Content-Type-Options', 'nosniff')
h.set('Referrer-Policy', 'strict-origin-when-cross-origin')
h.set('X-Frame-Options', 'DENY')
}
```
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](https://github.com/rsc-kit/rsc-kit/issues), and until then a
policy that allows your own origin's scripts is the honest one.
## Cookie options
```ts
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:
```ts title="vite.config.ts"
rscKit({ identify: false })
```