@k8ordo/server

リクエストとCookieを読む

このモードでは、ページがリクエストのヘッダーとCookieを読めます。一方でCookieを書くのはページではなく、リクエストに答える側のguard.tsやroute.ts、Server Actionです。このページでは、読む方法と書く方法を順に説明します。

このページの内容

ページからリクエストを読む

ページとレイアウト、not-found.tsxは、paramsとpathnameの横にrequestを受け取ります。中身はリクエストのヘッダーと、名前ごとに読み取ったCookieです。

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:リクエストのヘッダーです。型はHeadersです。
  • request.cookies:Cookieヘッダーを名前ごとに読み取ったReadonlyMapです。同じ名前が2回あれば最初のものを使い、値を囲む引用符を外してURLデコードします。

PagePropsとLayoutPropsがrequestを持つのは、生成されたregister.gen.tsに、このモードにはリクエストがあると書かれているからです。下のコンポーネントにpropとして渡すときは、@k8ordo/server/runtimeのRouteRequestを型に使います。

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>;
}

クライアントコンポーネントにはrequestを丸ごと渡さず、要る値だけを取り出して渡します。Headersは、境界を越えられないからです。

requestにsearchは入りません。?から後ろは@k8ordo/stateのもので、ブラウザで読みます。

cookies()でCookieを読み書きする

guard.tsやroute.ts、Server Actionの中では、@k8ordo/server/runtimeのcookies()でCookieを読み書きできます。次の例は、ログインに成功したらセッションのCookieを書いて、アカウントのページへ送るアクションです。

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'));
}

読むと、リクエストが運んできたCookieに、同じリクエストの中で先に書いたものが重なって見えます。guardが書いた値は、その後に走るServer Actionが読めます。

書いたものは、答えが何であっても、その答えのSet-Cookieとしてブラウザに届きます。アクションが描き直したページでも、redirect()の303でも同じです。

値はSet-Cookieに載せるときにパーセントエンコードされ、リクエストで戻ってくるときにデコードされます。そのためsetにはどんな文字列もそのまま渡せます。エンコード済みの値を渡すと、二重にエンコードされます。

情報

メモ

アクションの後に描き直すページがrequest.cookiesで見るのは、リクエストが運んできたCookieです。アクションが書いた値ではありません。

Cookieの属性を決める

setの3つ目の引数で、Cookieの属性を渡します。何も渡さなければ、セッションに合った値になります。

  • path:既定は/です。
  • domain:既定では付けません。
  • maxAge:有効期間を秒で渡します。0を渡すと、すぐに切れます。
  • expires:期限をDateで渡します。
  • httpOnly:既定はtrueで、ページのスクリプトからは読めません。
  • secure:既定はtrueで、HTTPSでしか送られません。
  • sameSite:'strict'か'lax'、'none'のどれかで、既定は'lax'です。'none'はsecureのときだけ使えます。

ただし、localhostと127.0.0.1、[::1]に素のHTTPで届いたリクエストだけは、secureの既定がfalseになります。ChromiumとFirefoxはそこでもSecureのCookieを残しますが、Safariは捨ててしまうからです。

それ以外の場所を素のHTTPで配信するなら、secure: falseを渡します。

deleteには、書いたときのpathとdomainを渡します。ブラウザはCookieを、この2つで見分けるからです。

Server Actionでリクエストのヘッダーを読む

Server Actionが受け取るのは引数で、リクエストではありません。アクションの中でヘッダーを読むときは、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);
}

requestHeaders()もcookies()と同じく、guard.tsやroute.ts、Server Actionの中でだけ使えます。guard.tsとroute.tsはrequestを受け取るので、ふつうはそちらを読みます。

ページが応答を書かない理由

ページには、ステータスやSet-Cookieを書く手段がありません。ページは描画であり、応答を書く描画は2つ目のハンドラになってしまうからです。

応答にページ以外の何を付けるかは、ページより前にguard.tsが決めるか、POSTが運んできたServer Actionが決めます。ページの中でcookies()やresponseHeaders()を呼ぶと、例外を投げます。

requestはこのモードにしかありません。@k8ordo/staticでは生成される型にrequestが無いので、それを読むページは型の検査で落ちます。

k8ordo

Baselineに入った機能を、制限なく使うReactのライブラリ群。

© 2026 k8o — MIT License

組版:Noto Sans JP / M PLUS 2