@k8ordo/server

Read the request and cookies

In this mode a page can read the request’s headers and cookies. Writing a cookie is not the page’s job, though: it belongs to what answers the request, a guard.ts, a route.ts or a Server Action. This page covers reading first, then writing.

On this page

Read the request from a page

A page, a layout and a not-found.tsx receive request beside params and pathname: the request’s headers, and its cookies read by name.

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

export default function RootLayout({
  children,
  request,
}: LayoutProps<'/'>) {
  const theme = request.cookies.get('theme') ?? 'light';
  return (
    <html data-theme={theme} lang="en">
      <body>{children}</body>
    </html>
  );
}
  • 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, and surrounding quotes are removed before the value is URL-decoded.

PageProps and LayoutProps carry request because the generated register.gen.ts says this mode has one. To hand it to a component further down as a prop, type it with RouteRequest from @k8ordo/server/runtime.

src/routes/_parts/greeting.tsx
import type { RouteRequest } from '@k8ordo/server/runtime';

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

Do not hand request whole to a client component; take out the values it needs. Headers cannot cross the boundary.

The search is not in request. Everything after the ? belongs to @k8ordo/state, and is read in the browser.

Read and write cookies with cookies()

Inside a guard.ts, a route.ts or a Server Action, cookies() from @k8ordo/server/runtime reads and writes cookies. Below, an action that writes a session cookie on a successful sign-in, then sends the visitor to their account.

src/routes/login/_parts/sign-in.ts
'use server';

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

import { startSession } from '../../_data/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, {
    maxAge: 60 * 60 * 24 * 30,
  });
  redirect(href('/account'));
}

A read sees the cookies the request carried, with what was written earlier in the same request on top, so a Server Action reads what a guard before it wrote.

Every write reaches the browser as a Set-Cookie on the answer, whatever the answer is: the page an action re-rendered, or the 303 of a redirect().

A value is percent-encoded on its way into Set-Cookie and decoded when a request brings it back, so set takes any string as it is. An already encoded value would be encoded twice.

Information

Note

The page re-rendered after an action sees, in request.cookies, the cookies the request carried — not what the action wrote.

Choose the cookie’s attributes

Pass the cookie’s attributes as the third argument of set. Left out, they are what a session wants.

  • path: / by default.
  • domain: not set by default.
  • maxAge: how long it lasts, in seconds. 0 expires it at once.
  • expires: when it expires, as a Date.
  • httpOnly: true by default, so a page’s script cannot read it.
  • secure: true by default, so it is sent over HTTPS only.
  • sameSite: 'strict', 'lax' or 'none', 'lax' by default. 'none' only goes with secure.

Over plain HTTP to this machine (localhost, 127.0.0.1, [::1]), though, secure defaults to false: Chromium and Firefox keep a Secure cookie there, but Safari drops it.

Anywhere else served over plain HTTP, pass secure: false.

Pass delete the path and domain the cookie was written with, since a browser tells cookies apart by those.

Read the request’s headers in a Server Action

A Server Action receives its arguments, not the request. Inside one, read the headers with requestHeaders().

src/routes/_parts/report.ts
'use server';

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

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

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

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

Why a page does not write the response

A page has no way to write a status or a Set-Cookie. A page is a render, and a render that wrote the response would be a second handler.

What the answer carries beyond the page is decided before it, by a guard.ts, or by the Server Action a POST carries. Calling cookies() or responseHeaders() while a page renders throws.

request exists only in this mode. Under @k8ordo/static the generated types do not carry it, so a page that reads it fails to type-check.

k8ordo

React libraries that use Baseline features without holding back.

© 2026 k8o — MIT License

Typeset in Noto Sans JP & M PLUS 2