# `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 from `crypto.getRandomValues`, with a `Math.random` fallback only when WebCrypto is missing. - `parseUrl(url)` — returns `URL` or `null`; tolerates absolute and relative inputs without throwing. - `safeReadBody(request)` — JSON body reader that enforces a 5 MiB cap and converts size or parser errors into `HOST.REQUEST_VALIDATION_FAILED`. - `toResponse(value)` — wraps handler return values as `Response` (passes through existing `Response` unchanged). - `withRequestHeaders(response, id)` — clones a `Response` with `X-Request-Id` set. - `buildRequestContext(args)` — assembles the typed `RequestContext` (including the documented `tenant`-may-be-null escape hatch for the foundation-gap-5 default behaviour). - `DEFAULT_IDEMPOTENCY_TTL_MS` (10 minutes) and `HANDLER_VALIDATION_LIMIT_BYTES` (5 MiB) constants. ## Usage: Basic ```ts 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: ```ts 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 ```ts 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; 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.