@k8ordo/framework

Request and cookies

Server mode only. Read the request’s headers and cookies from a page, and write cookies from a guard.ts, a route.ts or a Server Action.

On this page

Reading request

src/routes/layout.tsx
import type { LayoutProps } from '@k8ordo/framework';

export default function RootLayout({
  children,
  request,
}: LayoutProps<'/'>) {
  const theme = request.cookies.get('theme') ?? 'light';
Reads a cookie by name
  return (
    <html data-theme={theme} lang="en">
      <body>{children}</body>
    </html>
  );
}

A page, a layout and a not-found.tsx receive request alongside params and pathname.

  • request.headers: the request’s headers, as Headers.
  • request.cookies: the Cookie header read by name into a ReadonlyMap. The first of a repeated name wins. Surrounding quotes are removed and the value is percent-decoded.

The generated register.gen.ts adds request to PageProps and LayoutProps under server mode only. Under static mode, a page that reads request is a type error.

src/components/greeting.tsx
import type { RouteRequest } from '@k8ordo/framework/server';

export function Greeting({ request }: { request: RouteRequest }) {
  const name = request.cookies.get('name') ?? 'there';
  return <p>Hello, {name}</p>;
}

To pass request to a child Server Component, type the prop with RouteRequest from @k8ordo/framework/server. Headers cannot be passed to a Client Component, so give a Client Component only the values it needs. The query is not part of request. A page declares the query it reads by exporting search, as described on Parameters.

Reading and writing with cookies()

src/lib/sign-in.ts
'use server';

import { href } from '@k8ordo/framework';
import { cookies, redirect } from '@k8ordo/framework/server';

import { startSession } from './sessions.server';

export type SignInState = { error?: string };

export async function signIn(
  _previous: SignInState,
  formData: FormData,
): Promise<SignInState> {
  const session = await startSession(formData);
  if (session === null) return { error: 'Wrong password' };
  cookies().set('session', session.token, {
Sent as `Set-Cookie` on the response
    maxAge: 60 * 60 * 24 * 30,
  });
  redirect(href('/account'));
}

Inside a guard.ts, a route.ts or a Server Action, cookies() from @k8ordo/framework/server reads and writes cookies. The action above writes a session cookie after a successful sign-in, then redirects to /account.

A get reflects whatever was set or deleted earlier in the same request, so a Server Action reads what a guard.ts before it wrote. Every set goes out as a Set-Cookie on whichever response answers: the page re-rendered after an action, the 303 of a redirect(), or the payload the client runtime applies.

A value is percent-encoded when written to Set-Cookie and decoded when the next request brings it back. Pass set the raw string. An already encoded value is encoded twice.

Information

Note

In the page re-rendered after an action, request.cookies holds only the request’s cookies. What the action wrote is not in it.

Cookie attributes

ts
cookies().set('session', token, { maxAge: 3600, sameSite: 'strict' });

Pass the cookie’s attributes as the third argument of set. An attribute left out takes a default suited to a sign-in session cookie.

  • path: / by default.
  • domain: not set by default.
  • maxAge: the lifetime in seconds. 0 expires the cookie at once.
  • expires: the expiry, as a Date.
  • httpOnly: true by default, so page scripts cannot read the cookie.
  • secure: true by default, so the cookie is sent over HTTPS only.
  • sameSite: 'strict', 'lax' or 'none', 'lax' by default. With 'none', secure defaults to true even on localhost. Combining it with secure: false fails with a cookie with sameSite "none" has to be secure.

Only for a request over plain HTTP to localhost, 127.0.0.1 or [::1] does secure default to false: Chromium and Firefox keep a Secure cookie there, but Safari does not. To serve any other host over plain HTTP, pass secure: false.

Pass delete the path and domain the cookie was written with. A browser tells cookies apart by those two.

Cookie state from @k8ordo/state

src/routes/layout.tsx
import type { LayoutProps } from '@k8ordo/framework';

import { Shell } from '../components/shell';
import { density } from '../state';

export default function RootLayout({
  children,
  request,
}: LayoutProps<'/'>) {
  return (
    <Shell initialCookie={density.parseCookies(request.cookies)}>
Reads the values through the schema, once
      {children}
    </Shell>
  );
}

Cookie state from @k8ordo/state is read by passing request.cookies to the definition’s parseCookies. Pass the result to useAppState as initialCookie, and the server render already uses them.

initialCookie seeds only the useAppState call it is passed to. Read the cookie once in a layout, and pass the result down to the Client Component as a prop. Defining the state is described on Cookie.

src/lib/compact.ts
'use server';

import { cookies } from '@k8ordo/framework/server';

import { density } from '../state';

export async function compact() {
  cookies().set(
    density.cookieName,
    density.cookieValue({ density: 'compact' }),
    { httpOnly: false, maxAge: 34_560_000 },
The attributes the browser writes
  );
}

To write the same cookie with cookies(), pass cookieName as the name and the return value of cookieValue() as the value. cookieValue() runs the values through the schema and fills the fields you left out with their defaults. Set maxAge to 400 days (34_560_000 seconds), the same the browser writes.

Warning

Pitfall

Keep httpOnly: false. A script cannot read an HttpOnly cookie, so useAppState in the browser would no longer see the value.

requestHeaders()

src/lib/report.ts
'use server';

import { requestHeaders } from '@k8ordo/framework/server';

import { saveReport } from './reports.server';

export async function report(formData: FormData): Promise<void> {
  const agent = requestHeaders().get('user-agent') ?? 'unknown';
Reads the request’s headers
  await saveReport(formData.get('body'), agent);
}

A Server Action receives only its arguments, not the request. Read the headers with requestHeaders().

Like cookies(), requestHeaders() works only inside a guard.ts, a route.ts or a Server Action. A guard.ts and a route.ts receive the Request itself, so they usually read request.headers instead.

Writing to the response

A page has no way to write a status or a Set-Cookie. Only a guard.ts, or the Server Action a POST called, can write them, and both run before the page renders. Calling cookies() or responseHeaders() while a page renders fails with an error containing belongs to what answers the request.

k8ordo

React libraries that use Baseline features without holding back.

© 2026 k8o — MIT License

Typeset in Noto Sans JP & M PLUS 2