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

4.6 KiB

request-pipeline-outcomes — driver config + success/failure result builders

Purpose

This module ties the bare emitters and validators together for the orchestrator. It exposes DriverConfig (the frozen per-pipeline settings bundle), OutcomeArgs (the common argument shape carried through every gate-fail branch), runRequestValidation (a thin adapter that parses the URL query, short-circuits on streaming / opt-out routes, and emits the dedicated api.validation.failures_total{where=request} counter), and the successResult / failResult builders that translate an outcome into a fully-shaped PipelineResult.

These helpers were extracted from request-pipeline.ts during the file-size refactor. Behaviour is byte-for-byte identical to the pre-split implementation; the only change is that the shared argument bundle is now reified as a type so each branch can use ...spread instead of re-listing eight parameters.

Features

  • DriverConfig freezes the constructor inputs once and is threaded through every helper, so step-level functions see the same boundary, now, instruments, defaultTenant, and responseValidation mode.
  • runRequestValidation is the only piece that knows about the request URL's query — the orchestrator does not parse it directly anymore.
  • successResult always increments api.requests_total{outcome=success} and the duration histogram before returning.
  • failResult increments the same counter/histogram with outcome=failure; the audit envelope is only emitted when a route matched (the 404 path skips audit per spec).
  • OutcomeArgs is exported so the orchestrator can declare a single carry variable and reuse it across every fail branch.

Usage: Basic

import {
    type DriverConfig,
    failResult,
    runRequestValidation,
    successResult,
} from "@suaveplan/api-host/request-pipeline-outcomes";

const validation = await runRequestValidation(config, { request, route, rootSpan });
if (validation.kind === "fail") {
    return failResult(config, { ...carry, failure: validation.failure });
}
return successResult(config, { ...carry, response: handlerResponse });

Usage: Advanced

DriverConfig decouples the orchestrator from the RequestPipeline class — tests can construct a config object directly and call runPipeline to verify behaviour without instantiating the full class:

const config: DriverConfig = {
    options,
    instruments,
    boundary: new ErrorBoundary({ instruments, logger }),
    now: () => 1000,
    idempotencyTtlMs: 60_000,
    responseValidation: "enforce",
    defaultTenant: null,
};

OutcomeArgs.route is nullable so the 404 path can reuse the same failResult shape. Tests that assert "no audit envelope on 404" rely on this branch — supplying a non-null route would silently emit an audit entry.

API Reference

interface DriverConfig {
    options: RequestPipelineOptions;
    instruments: HostInstruments;
    boundary: ErrorBoundary;
    now: () => number;
    idempotencyTtlMs: number;
    responseValidation: "enforce" | "warn";
    defaultTenant: TenantContext | null;
}

interface OutcomeArgs {
    request: Request;
    requestId: string;
    rootSpan: OtelSpanLike & BoundarySpan;
    startedAt: number;
    route: PipelineRoute | null;
    principal: Principal | null;
    tenantCtx: TenantContext | null;
}

function runRequestValidation(
    config: DriverConfig,
    args: { request: Request; route: PipelineRoute; rootSpan: OtelSpanLike & BoundarySpan }
): Promise<RequestValidationOk | RequestValidationFail>;

function successResult(config: DriverConfig, args: OutcomeArgs & { response: Response }): Promise<PipelineResult>;
function failResult(config: DriverConfig, args: OutcomeArgs & { failure: BoundaryFailure }): Promise<PipelineResult>;

Implementation Notes

runRequestValidation is the ONLY function that derives parsedQuery from the request URL. Centralising it ensures all three early-validation outcomes (streaming-skip, opt-out, schema-pass) return the same query record shape — without that, the handler ctx could see {} on one path and the parsed record on another.

successResult calls withRequestHeaders exactly once, deliberately. The original implementation also called it once and the post-extracted form preserves that — re-cloning the response would burn an unnecessary Headers allocation per request.

failResult always emits the api.requests_total{outcome=failure} counter, including on the 404 path. That contract is depended on by the api.errors_by_step dashboard the host's STATUS docs reference; removing it (e.g. as an optimisation for the no-route case) would silently break the metric.