Files
suaveplan/packages/api-stack/api/api-host/src/request-pipeline/request-pipeline-helpers.md
2026-05-27 23:29:53 +01:00

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

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.