Request Context

Read request headers and pass request-scoped data to server components and server functions.


When to Use Request Context

Waku provides request-scoped context for server code. Use it when a server component or server function needs information from the current request. Middleware runs before the render scope is established, so middleware reads the request from its Hono context instead.

Read the Current Request

Use getRequest in server-only code to access the current Request:

import { getRequest } from 'waku/router/server';

export default function Page() {
  const userAgent = getRequest().headers.get('user-agent') ?? 'unknown';

  return <p>User agent: {userAgent}</p>;
}

export const getConfig = async () => ({ render: 'dynamic' }) as const;

Header names are case-insensitive, and headers.get() returns null for a header the request does not have.

getRequest() throws if request context is not available. Call it during request handling, not in module scope.

getRequest().url is the URL the request was sent to. After a client navigation, that is Waku's RSC URL (such as /RSC/R/about.txt), not the page URL. A page receives its params and query as props; elsewhere, read the route with unstable_parseRouterRequest(getRequest()). A server function called from the client sends its own request, which unstable_parseRouterRequest reports only as { type: 'action' }, so pass the function any route data it needs as arguments.

getRequest() returns the original incoming request. Inside an API route handler, the handler's own req argument may have a rewritten URL (the route path with any prefix stripped), so req.url and getRequest().url can differ. Use the handler argument for routing-specific values and for the body, which it shares with getRequest().

getRequest is exported from waku/router/server and is available wherever the router runs your code: renders (request and build), server functions, API route handlers, interceptors, and a custom router's resolve. In the Minimal API, use input.req from handleRequest instead, and bring your own AsyncLocalStorage if you need it deeper in the tree.

Components rendered as static are prerendered at build time, so do not read getRequest, headers, or interceptor-seeded state in a static page, layout, or slice; those values would be baked into the prerendered output. Read request-scoped data only in dynamic rendering. File-system pages, layouts, and slices are static by default, so the page examples in this guide export getConfig with render: 'dynamic'. Only the file-system router reads getConfig; with createPages, pass render: 'dynamic' to createPage instead. (During a static route build getRequest().url is the route's path, but a static slice build uses a placeholder root request that does not identify the slice.)

Pass Data with an Interceptor

Waku's context is request-only. To share derived, request-scoped data with server components and server functions, bring your own AsyncLocalStorage and seed it from a handler interceptor. An interceptor wraps each render in both the request and build phases, so getRequest is available inside it.

Define your own store:

// src/lib/request-data.ts
import { AsyncLocalStorage } from 'node:async_hooks';

export type RequestData = {
  country?: string;
};

const store = new AsyncLocalStorage<RequestData>();

export const getRequestData = (): RequestData => store.getStore() ?? {};

export const runWithRequestData = <T>(
  data: RequestData,
  fn: () => Promise<T>,
) => store.run(data, fn);

In managed mode (no waku.server.tsx), seed the store from an interceptor in src/pages/_interceptors/request-data.ts:

import type { HandlerInterceptor } from 'waku/router/server';
import { getRequest } from 'waku/router/server';
import { runWithRequestData } from '../../lib/request-data.js';

const requestDataInterceptor: HandlerInterceptor = (next) => {
  const country = getRequest().headers.get('cf-ipcountry') ?? 'unknown';
  return runWithRequestData({ country }, next);
};

export default requestDataInterceptor;

With a custom waku.server.tsx using createPages, register the same logic through createInterceptor:

import { getRequest } from 'waku/router/server';
import { runWithRequestData } from './lib/request-data.js';

createPages(async ({ createPage, createInterceptor }) => {
  createInterceptor((next) => {
    const country = getRequest().headers.get('cf-ipcountry') ?? 'unknown';
    return runWithRequestData({ country }, next);
  });
  return [
    // ...pages...
  ];
});

Then read that data from server code:

import { getRequestData } from '../lib/request-data.js';

export default function Page() {
  const { country } = getRequestData();

  return <p>Country: {country ?? 'unknown'}</p>;
}

export const getConfig = async () => ({ render: 'dynamic' }) as const;

If the value is just a request header, you can also read it from getRequest().headers in the component without a store. Reach for an interceptor when you want to compute the value once per request and share it across the render.

Cookies

Waku does not currently provide a dedicated cookie API. Read request cookies from getRequest().headers in server code, and use middleware to set response cookies after rendering.

Read a request cookie from server code with the cookie package:

import * as cookie from 'cookie';
import { getRequest } from 'waku/router/server';

export default function Page() {
  const cookies = cookie.parseCookie(getRequest().headers.get('cookie') ?? '');

  return <p>Session: {cookies.sessionId ?? 'none'}</p>;
}

export const getConfig = async () => ({ render: 'dynamic' }) as const;

Set a response cookie from middleware, which owns the response:

// src/middleware/session.ts
import * as cookie from 'cookie';
import type { MiddlewareHandler } from 'hono';

const sessionMiddleware = (): MiddlewareHandler => {
  return async (c, next) => {
    const cookies = cookie.parseCookie(c.req.header('cookie') || '');
    const sessionId = cookies.sessionId;

    await next();

    if (c.res && sessionId) {
      const headers = new Headers(c.res.headers);
      headers.append(
        'set-cookie',
        cookie.stringifySetCookie({
          name: 'sessionId',
          value: sessionId,
          httpOnly: true,
          path: '/',
          sameSite: 'lax',
          secure: true,
        }),
      );
      c.res = new Response(c.res.body, {
        status: c.res.status,
        statusText: c.res.statusText,
        headers,
      });
    }
  };
};

export default sessionMiddleware;

Keep any request-scoped store small and request-specific. Do not store values that should outlive the request.

Cloudflare Bindings

On Cloudflare Workers, import bindings from cloudflare:workers. Use Waku request context for request data, and Cloudflare's runtime APIs for environment bindings, D1, KV, and waitUntil.

import { env } from 'cloudflare:workers'; // eslint-disable-line import/no-unresolved
import { getRequest } from 'waku/router/server';

export default async function Page() {
  const country = getRequest().headers.get('cf-ipcountry');
  const store = country
    ? await env.DB.prepare('SELECT * FROM store WHERE country = ?')
        .bind(country)
        .first()
    : null;

  return <pre>{JSON.stringify(store, null, 2)}</pre>;
}

export const getConfig = async () => ({ render: 'dynamic' }) as const;

See Run Waku on Cloudflare for the full Cloudflare setup.

designed bycandycode alternative graphic design web development agency San Diego