4.0 KiB
request-pipeline-helpers — pure utilities for the request pipeline
Purpose
This module concentrates the pure (no-this, no side-effecting) helpers the §6 request pipeline relies on: request-id generation, URL parsing tolerant of relative forms, JSON body reader with a size cap, Response wrapping for handler-returned values, an X-Request-Id header stamper, and the RequestContext builder handed to plugin handlers. The split is mechanical — every function used to live inside request-pipeline.ts and was extracted verbatim when the file outgrew the CLAUDE.md per-file LOC budget.
Keeping these helpers in their own module lets the driver, the step gates, and the outcome emitters all consume them without circular imports, and lets each helper be unit-tested via the existing end-to-end pipeline tests with zero behavioural drift.
Features
defaultRequestId()— 32-char hex string drawn fromcrypto.getRandomValues, with aMath.randomfallback only when WebCrypto is missing.parseUrl(url)— returnsURLornull; tolerates absolute and relative inputs without throwing.safeReadBody(request)— JSON body reader that enforces a 5 MiB cap and converts size or parser errors intoHOST.REQUEST_VALIDATION_FAILED.toResponse(value)— wraps handler return values asResponse(passes through existingResponseunchanged).withRequestHeaders(response, id)— clones aResponsewithX-Request-Idset.buildRequestContext(args)— assembles the typedRequestContext(including the documentedtenant-may-be-null escape hatch for the foundation-gap-5 default behaviour).DEFAULT_IDEMPOTENCY_TTL_MS(10 minutes) andHANDLER_VALIDATION_LIMIT_BYTES(5 MiB) constants.
Usage: Basic
import {
buildRequestContext,
defaultRequestId,
parseUrl,
toResponse,
withRequestHeaders,
} from "@suaveplan/api-host/request-pipeline-helpers";
const id = defaultRequestId();
const url = parseUrl(request.url);
const response = withRequestHeaders(toResponse({ ok: true }), id);
Usage: Advanced
safeReadBody is the size-bounded JSON reader the request-validation step uses; consumers needing identical semantics outside the pipeline (e.g. an admin endpoint) can call it directly:
import { safeReadBody } from "@suaveplan/api-host/request-pipeline-helpers";
try {
const body = await safeReadBody(request);
// body is `unknown`; downstream Zod parse expected.
} catch (cause) {
// HOST.REQUEST_VALIDATION_FAILED — already shaped for the boundary.
throw cause;
}
API Reference
const DEFAULT_IDEMPOTENCY_TTL_MS: number;
const HANDLER_VALIDATION_LIMIT_BYTES: number;
function defaultRequestId(): string;
function parseUrl(url: string): URL | null;
function safeReadBody(request: Request): Promise<unknown>;
function toResponse(value: unknown): Response;
function withRequestHeaders(response: Response, requestId: string): Response;
function buildRequestContext(args: BuildRequestContextArgs): RequestContext;
Implementation Notes
Every helper is referentially transparent except safeReadBody (which reads the request stream) and defaultRequestId (which calls into WebCrypto). All other functions take their inputs by parameter and never touch module-scoped state, so they are safe to call from any tier — universal, server, browser, or test.
buildRequestContext performs a documented cast for tenant/tenantId: the shared RequestContext type declares both as non-nullable for typed-handler ergonomics, but foundation-gap-5 made the runtime value nullable when the resolver chain misses and defaultTenant is null (the new default). Handlers consuming tenant-bound capabilities must defensively gate on ctx.tenant === null.
The 5 MiB limit on safeReadBody is intentional — larger payloads should use a streaming upload route (streaming: true) that bypasses request validation buffering entirely. The constant is exported so operators can audit it without grepping the implementation.