Files
clean-architecture-frontend…/src/adapters/http/http-execution-v3.ts
T

1227 lines
35 KiB
TypeScript

import {
HTTP_EXECUTION_CEILINGS,
invokeValidator,
type InstalledHttpContract,
} from "../../contracts/external-contract-runtime.ts";
import type { CacheScopeSnapshot } from "../../contracts/server-state-scope.ts";
import {
defineMutationIntent,
MUTATION_INTENT_BOUNDS,
type MutationIntent,
} from "../../contracts/mutation-intent.ts";
import {
decodeJsonBytes,
isEffectivelyEmpty,
isJsonMediaType,
probeForbiddenBody,
readBoundedBytes,
} from "./bounded-body-reader.ts";
import {
checkFinalInvariants,
projectRequest,
type CredentialPatchOutcome,
} from "./http-contract-bridge.ts";
import {
certaintyForAbandonedAttempt,
classifyProblemEffect,
type PhysicalAttemptState,
} from "./http-effect-certainty.ts";
import { parseRetryAfter } from "./retry-policy.ts";
/**
* §7–§8. Descriptor-driven HTTP execution.
*
* This is not a general purpose HTTP client. A caller passes an installed
* contract and typed input; the runtime owns bounds, the total deadline, the
* single retry authority and the effect-certainty verdict.
*/
export type SafeResponseMetadata = Readonly<{
status: number;
correlationReference?: string;
retryAfterMs?: number;
validatorToken?: string;
}>;
export type HttpContractViolationKind =
| "MISSING_IDEMPOTENCY_KEY"
| "UNEXPECTED_IDEMPOTENCY_KEY"
| "UNEXPECTED_STATUS"
| "UNEXPECTED_EMPTY_BODY"
| "UNEXPECTED_BODY"
| "CONTENT_TYPE_MISMATCH"
| "RESPONSE_TOO_LARGE"
| "UTF8_INVALID"
| "JSON_INVALID"
| "SUCCESS_SCHEMA_INVALID"
| "PROBLEM_SCHEMA_INVALID"
| "VALIDATOR_RUNTIME_FAILURE"
| "MAPPING_CONTRACT_VIOLATION"
| "FINAL_REQUEST_INVARIANT_FAILED"
| "SCOPE_FENCED";
export type HttpContractViolation = Readonly<{
kind: HttpContractViolationKind;
operation: "REQUEST" | "RESPONSE" | "VALIDATION" | "MAPPING";
}>;
export type HttpEffectCertainty =
| "NOT_APPLICABLE"
| "NOT_STARTED"
| "NOT_APPLIED"
| "APPLIED_CONFIRMED"
| "MAYBE_APPLIED";
export type HttpTransportFailure = Readonly<{
kind:
| "DNS_OR_CONNECT_FAILURE"
| "TLS_OR_NETWORK_FAILURE"
| "NETWORK_FAILURE"
| "TIMEOUT"
| "RESPONSE_STREAM_FAILURE"
| "ABORTED_BY_SCOPE"
| "ABORTED_BY_CALLER"
| "OVERLOADED_BEFORE_SEND";
retryable: boolean;
}>;
export type HttpExecutionOutcome<Value, Problem> =
| Readonly<{
kind: "SUCCESS";
value: Value;
metadata: SafeResponseMetadata;
effect: "NOT_APPLICABLE" | "APPLIED_CONFIRMED";
}>
| Readonly<{
kind: "PROBLEM";
problem: Problem;
metadata: SafeResponseMetadata;
effect: "NOT_APPLIED" | "APPLIED_CONFIRMED" | "MAYBE_APPLIED";
}>
| Readonly<{
kind: "UNAUTHENTICATED";
effect: "NOT_APPLICABLE" | "NOT_APPLIED" | "MAYBE_APPLIED";
}>
| Readonly<{
kind: "FORBIDDEN";
effect: "NOT_APPLICABLE" | "NOT_APPLIED" | "MAYBE_APPLIED";
}>
| Readonly<{
kind: "RATE_LIMITED";
retryAfterMs?: number;
effect: "NOT_APPLICABLE" | "NOT_APPLIED" | "MAYBE_APPLIED";
}>
| Readonly<{
kind: "CONTRACT_VIOLATION";
violation: HttpContractViolation;
effect: HttpEffectCertainty;
}>
| Readonly<{
kind: "TRANSPORT_FAILURE";
failure: HttpTransportFailure;
effect: "NOT_STARTED" | "MAYBE_APPLIED";
}>
| Readonly<{
kind: "CANCELLED";
effect: "NOT_STARTED" | "MAYBE_APPLIED";
}>;
export type CancellationOwner =
| "CALLER"
| "ROUTE_TRANSITION"
| "SCOPE_FENCE"
| "APPLICATION_SHUTDOWN"
| "DEADLINE";
export interface HttpExecutionContext {
readonly signal?: AbortSignal;
readonly scope: CacheScopeSnapshot;
readonly intent?: MutationIntent;
}
export interface ContractHttpExecutor {
execute<Input, WireOutput, Problem>(
operation: InstalledHttpContract<Input, WireOutput, Problem>,
input: Input,
context: HttpExecutionContext,
): Promise<HttpExecutionOutcome<WireOutput, Problem>>;
}
export type HttpExecutionObservation = Readonly<{
diagnosticsOperation: string;
outcome: string;
attempts: number;
certainty: string;
}>;
export type ContractHttpExecutorDependencies = Readonly<{
baseUrl: string;
/** §8.2. `MAX_RETRY_ATTEMPTS` from Runtime Config; the ceiling is still 2. */
maxRetryAttempts: number;
attachCredentials(
operation: Readonly<{
operationId: string;
authProfileId: string;
method: string;
}>,
): Promise<CredentialPatchOutcome> | CredentialPatchOutcome;
fetcher?: typeof fetch;
/** Adapter seam for the common bounded response reader. */
readBoundedResponseBytes?: typeof readBoundedBytes;
monotonicNow?: () => number;
sleep?: (ms: number, signal: AbortSignal) => Promise<void>;
random?: () => number;
observe?: (observation: HttpExecutionObservation) => void;
}>;
const RETRYABLE_STATUSES: ReadonlySet<number> = new Set([
408, 425, 429, 502, 503, 504,
]);
const RETRY_BASE_DELAY_MS = 250;
const RETRY_MAX_LOCAL_DELAY_MS = 2_000;
const RETRY_AFTER_CEILING_MS = 5_000;
type MutationIntentValidation =
| Readonly<{ ok: true; intent?: MutationIntent }>
| Readonly<{
ok: false;
violation: "MISSING_IDEMPOTENCY_KEY" | "UNEXPECTED_IDEMPOTENCY_KEY";
}>;
function validateMutationIntent(
contract: Readonly<{
operationId: string;
retrySemantics: "SAFE" | "IDEMPOTENT" | "KEYED" | "NEVER";
commandEffect: unknown | null;
}>,
intent: MutationIntent | undefined,
): MutationIntentValidation {
const isCommand = contract.commandEffect !== null;
const requiresKey = contract.retrySemantics === "KEYED";
if (!isCommand) {
return intent === undefined
? Object.freeze({ ok: true })
: Object.freeze({
ok: false,
violation: "UNEXPECTED_IDEMPOTENCY_KEY",
});
}
if (intent === undefined) {
return requiresKey
? Object.freeze({ ok: false, violation: "MISSING_IDEMPOTENCY_KEY" })
: Object.freeze({ ok: true });
}
let validated: MutationIntent;
try {
validated = defineMutationIntent(intent);
} catch {
return Object.freeze({
ok: false,
violation: requiresKey
? "MISSING_IDEMPOTENCY_KEY"
: "UNEXPECTED_IDEMPOTENCY_KEY",
});
}
if (validated.operationId !== contract.operationId) {
return Object.freeze({
ok: false,
violation: requiresKey
? "MISSING_IDEMPOTENCY_KEY"
: "UNEXPECTED_IDEMPOTENCY_KEY",
});
}
const key = validated.idempotencyKey;
if (requiresKey && !validIdempotencyKey(key)) {
return Object.freeze({
ok: false,
violation: "MISSING_IDEMPOTENCY_KEY",
});
}
if (!requiresKey && key !== undefined) {
return Object.freeze({
ok: false,
violation: "UNEXPECTED_IDEMPOTENCY_KEY",
});
}
return Object.freeze({ ok: true, intent: validated });
}
const UTF8 = new TextEncoder();
function validIdempotencyKey(value: unknown): value is string {
return (
typeof value === "string" &&
value.trim().length > 0 &&
UTF8.encode(value).byteLength <=
MUTATION_INTENT_BOUNDS.idempotencyKeyMaxBytes &&
!hasControlCharacter(value)
);
}
function hasControlCharacter(value: string): boolean {
for (const character of value) {
const codePoint = character.codePointAt(0) ?? 0;
if (
codePoint <= 0x1f ||
(codePoint >= 0x7f && codePoint <= 0x9f)
) {
return true;
}
}
return false;
}
export function createContractHttpExecutor(
dependencies: ContractHttpExecutorDependencies,
): ContractHttpExecutor {
const fetcher = dependencies.fetcher ?? fetch;
const readResponseBytes =
dependencies.readBoundedResponseBytes ?? readBoundedBytes;
const now = dependencies.monotonicNow ?? (() => performance.now());
const random = dependencies.random ?? Math.random;
const sleep =
dependencies.sleep ??
((ms: number, signal: AbortSignal) =>
new Promise<void>((resolve) => {
const timer = setTimeout(resolve, ms);
signal.addEventListener(
"abort",
() => {
clearTimeout(timer);
resolve();
},
{ once: true },
);
}));
async function execute<Input, WireOutput, Problem>(
operation: InstalledHttpContract<Input, WireOutput, Problem>,
input: Input,
context: HttpExecutionContext,
): Promise<HttpExecutionOutcome<WireOutput, Problem>> {
const contract = operation.contract;
const policy = operation.frontend;
const isCommand = contract.commandEffect !== null;
// §8.5. One monotonic deadline covers credential resolution, encoding,
// backoff, every physical attempt, body read and validation.
const deadlineAt = now() + policy.totalDeadlineMs;
const remaining = () => deadlineAt - now();
let attemptState: PhysicalAttemptState = "PREPARING";
let attempts = 0;
let terminalCancellation: CancellationOwner | null = null;
const lifetimeController = new AbortController();
const forwardCallerToLifetime = () => {
terminalCancellation ??= "CALLER";
lifetimeController.abort();
};
const forwardScopeToLifetime = () => {
terminalCancellation ??= "SCOPE_FENCE";
lifetimeController.abort();
};
const callerSignal =
context.signal === context.scope.signal ? undefined : context.signal;
if (callerSignal?.aborted) forwardCallerToLifetime();
else {
callerSignal?.addEventListener("abort", forwardCallerToLifetime, {
once: true,
});
}
if (context.scope.signal.aborted) forwardScopeToLifetime();
else {
context.scope.signal.addEventListener("abort", forwardScopeToLifetime, {
once: true,
});
}
const lifetimeDeadlineTimer = setTimeout(() => {
terminalCancellation ??= "DEADLINE";
lifetimeController.abort();
}, policy.totalDeadlineMs);
let lifetimeDisposed = false;
const disposeLifetime = () => {
if (lifetimeDisposed) return;
lifetimeDisposed = true;
clearTimeout(lifetimeDeadlineTimer);
callerSignal?.removeEventListener("abort", forwardCallerToLifetime);
context.scope.signal.removeEventListener("abort", forwardScopeToLifetime);
lifetimeController.abort();
};
const finish = (
outcome: HttpExecutionOutcome<WireOutput, Problem>,
certainty: string,
): HttpExecutionOutcome<WireOutput, Problem> => {
disposeLifetime();
try {
dependencies.observe?.({
diagnosticsOperation: policy.diagnosticsOperation,
outcome: outcome.kind,
attempts,
certainty,
});
} catch {
// Observation is outside the execution authority.
}
return outcome;
};
try {
// §7.4 step 1-2: capture the scope and verify it is still current.
if (!context.scope.isCurrent()) {
return finish(scopeFenced(preDispatchEffect(isCommand)), "NOT_STARTED");
}
const intentValidation = validateMutationIntent(contract, context.intent);
if (!intentValidation.ok) {
return finish(
violation(intentValidation.violation, "REQUEST", "NOT_STARTED"),
"NOT_STARTED",
);
}
const validated = invokeValidator(contract.inputValidator, input);
if (validated.outcome === "THROWN") {
return finish(
violation(
"VALIDATOR_RUNTIME_FAILURE",
"VALIDATION",
preDispatchEffect(isCommand),
),
"NOT_STARTED",
);
}
if (validated.outcome === "INVALID") {
return finish(
violation(
"FINAL_REQUEST_INVARIANT_FAILED",
"REQUEST",
preDispatchEffect(isCommand),
),
"NOT_STARTED",
);
}
const projected = projectRequest(
operation,
validated.value,
dependencies.baseUrl,
);
if (!projected.ok) {
return finish(
violation(
"FINAL_REQUEST_INVARIANT_FAILED",
"REQUEST",
preDispatchEffect(isCommand),
),
"NOT_STARTED",
);
}
// §7.7 / §8.4. Credentials are resolved before send. A response 401 is
// terminal; there is no hidden refresh-and-replay.
let patchOperation: Promise<CredentialPatchOutcome>;
try {
patchOperation = Promise.resolve(
dependencies.attachCredentials({
operationId: contract.operationId,
authProfileId: policy.authProfileId,
method: contract.method,
}),
);
} catch {
return finish(unauthenticated("NOT_STARTED", isCommand), "NOT_STARTED");
}
const patchResult = await awaitWithAbort(
patchOperation,
lifetimeController.signal,
);
if (patchResult === ABORTED) {
if (terminalCancellation === "SCOPE_FENCE") {
return finish(
transportFailure(
"ABORTED_BY_SCOPE",
false,
attemptState,
isCommand,
),
"SCOPE_FENCED",
);
}
return terminalCancellation === "CALLER"
? finish(cancelled("NOT_STARTED"), "CANCELLED")
: finish(
transportFailure(
"TIMEOUT",
false,
attemptState,
isCommand,
),
"TIMEOUT",
);
}
const patch = patchResult;
if (patch.kind === "SCOPE_FENCED") {
return finish(scopeFenced(preDispatchEffect(isCommand)), "NOT_STARTED");
}
if (patch.kind !== "READY") {
// A missing credential never downgrades into an anonymous request.
return finish(unauthenticated("NOT_STARTED", isCommand), "NOT_STARTED");
}
if (
Object.keys(patch.headers).some(
(name) => name.toLowerCase() === "idempotency-key",
)
) {
return finish(
violation("UNEXPECTED_IDEMPOTENCY_KEY", "REQUEST", "NOT_STARTED"),
"NOT_STARTED",
);
}
const headers: Record<string, string> = {
Accept: "application/json",
...patch.headers,
};
if (contract.requestBody === "JSON") {
headers["Content-Type"] = "application/json";
}
if (intentValidation.intent?.idempotencyKey !== undefined) {
headers["Idempotency-Key"] = intentValidation.intent.idempotencyKey;
}
const retryCeiling = Math.min(
policy.retryBudget,
dependencies.maxRetryAttempts,
HTTP_EXECUTION_CEILINGS.hardRetryCount,
);
for (let retryIndex = 0; ; retryIndex += 1) {
if (callerSignal?.aborted) {
terminalCancellation ??= "CALLER";
return finish(
cancelled(certaintyForAbandonedAttempt(attemptState, isCommand)),
"CANCELLED",
);
}
if (!context.scope.isCurrent()) {
terminalCancellation ??= "SCOPE_FENCE";
return finish(
scopeFenced(contractViolationEffect(attemptState, isCommand)),
"SCOPE_FENCED",
);
}
const controller = new AbortController();
const budget = remaining();
if (budget <= 0) {
return finish(
transportFailure("TIMEOUT", false, attemptState, isCommand),
"TIMEOUT",
);
}
const deadlineTimer = setTimeout(() => {
terminalCancellation ??= "DEADLINE";
controller.abort();
}, budget);
const forwardLifetime = () => {
controller.abort();
};
if (lifetimeController.signal.aborted) forwardLifetime();
else {
lifetimeController.signal.addEventListener("abort", forwardLifetime, {
once: true,
});
}
const init: RequestInit = {
method: contract.method,
headers,
redirect: "error",
referrerPolicy: "no-referrer",
credentials: patch.credentials,
cache: "no-store",
signal: controller.signal,
...(projected.request.bodyBytes
? { body: projected.request.bodyBytes.slice() }
: {}),
};
const invariantFailure = checkFinalInvariants({
request: projected.request,
expectedMethod: contract.method,
baseUrl: dependencies.baseUrl,
init,
headers,
requestByteLimit: policy.requestByteLimit,
deadlineRemainingMs: remaining(),
scopeIsCurrent: context.scope.isCurrent(),
});
if (invariantFailure) {
clearTimeout(deadlineTimer);
lifetimeController.signal.removeEventListener("abort", forwardLifetime);
return finish(
invariantFailure === "SCOPE_FENCED"
? scopeFenced(preDispatchEffect(isCommand))
: violation(
"FINAL_REQUEST_INVARIANT_FAILED",
"REQUEST",
preDispatchEffect(isCommand),
),
"NOT_STARTED",
);
}
let response: Response;
attemptState = "READY_TO_SEND";
try {
attempts += 1;
const pending = fetcher(projected.request.url, init);
attemptState = "DISPATCHED";
response = await pending;
attemptState = "RESPONSE_HEADERS";
} catch {
clearTimeout(deadlineTimer);
lifetimeController.signal.removeEventListener("abort", forwardLifetime);
const owner = terminalCancellation;
if (owner === "CALLER") {
return finish(
cancelled(certaintyForAbandonedAttempt(attemptState, isCommand)),
"CANCELLED",
);
}
if (owner === "SCOPE_FENCE") {
return finish(
transportFailure(
"ABORTED_BY_SCOPE",
false,
attemptState,
isCommand,
),
"SCOPE_FENCED",
);
}
const kind = owner === "DEADLINE" ? "TIMEOUT" : "NETWORK_FAILURE";
if (
owner === null &&
canRetryTransport(contract.retrySemantics, attemptState) &&
retryIndex < retryCeiling &&
remaining() > 0
) {
const delay = jitteredDelay(retryIndex, random);
if (delay < remaining()) {
const slept = await awaitWithAbort(
sleep(delay, lifetimeController.signal),
lifetimeController.signal,
);
if (slept === ABORTED) {
return terminalCancellation === "CALLER"
? finish(
cancelled(
certaintyForAbandonedAttempt(attemptState, isCommand),
),
"CANCELLED",
)
: finish(
transportFailure(
"TIMEOUT",
false,
attemptState,
isCommand,
),
"TIMEOUT",
);
}
continue;
}
}
return finish(
transportFailure(kind, false, attemptState, isCommand),
kind,
);
}
try {
const outcome = await admitResponse(
operation,
response,
context,
attemptState,
readResponseBytes,
);
attemptState = "SETTLED";
if (
outcome.retryHint &&
retryIndex < retryCeiling &&
isRetryableSemantics(contract.retrySemantics)
) {
const delay = retryDelayFor(outcome.retryAfterMs, retryIndex, random);
if (delay !== null && delay < remaining()) {
clearTimeout(deadlineTimer);
lifetimeController.signal.removeEventListener(
"abort",
forwardLifetime,
);
const slept = await awaitWithAbort(
sleep(delay, lifetimeController.signal),
lifetimeController.signal,
);
if (slept === ABORTED) {
return terminalCancellation === "CALLER"
? finish(
cancelled(
certaintyForAbandonedAttempt(attemptState, isCommand),
),
"CANCELLED",
)
: finish(
transportFailure(
"TIMEOUT",
false,
attemptState,
isCommand,
),
"TIMEOUT",
);
}
continue;
}
}
return finish(outcome.result, outcome.certainty);
} finally {
clearTimeout(deadlineTimer);
lifetimeController.signal.removeEventListener("abort", forwardLifetime);
}
}
} catch {
return finish(
transportFailure("NETWORK_FAILURE", false, attemptState, isCommand),
"RUNTIME_FAILURE",
);
} finally {
disposeLifetime();
}
}
return Object.freeze({ execute });
}
type AdmissionOutcome<Value, Problem> = Readonly<{
result: HttpExecutionOutcome<Value, Problem>;
certainty: string;
retryHint: boolean;
retryAfterMs: number | null;
}>;
/**
* §7.9. Admission order is fixed: redirect, status allowlist, media type,
* declared length, bounded stream, body policy, decode, validate, scope fence.
* `response.ok` is never used on its own.
*/
async function admitResponse<Input, WireOutput, Problem>(
operation: InstalledHttpContract<Input, WireOutput, Problem>,
response: Response,
context: HttpExecutionContext,
attemptState: PhysicalAttemptState,
readResponseBytes: typeof readBoundedBytes,
): Promise<AdmissionOutcome<WireOutput, Problem>> {
const contract = operation.contract;
const policy = operation.frontend;
const isCommand = contract.commandEffect !== null;
const status = response.status;
const retryAfterMs = parseRetryAfter(response.headers.get("retry-after"));
if (response.redirected || response.type === "opaqueredirect") {
await response.body?.cancel().catch(() => {});
return settled(
violation(
"UNEXPECTED_STATUS",
"RESPONSE",
postDispatchEffect(isCommand),
),
"CONTRACT_VIOLATION",
);
}
const metadata = safeMetadata(response, retryAfterMs);
if (!contract.acceptedStatuses.includes(status)) {
if (status === 401) {
await response.body?.cancel().catch(() => {});
return settled(
unauthenticated(
certaintyForAbandonedAttempt(attemptState, isCommand),
isCommand,
),
"UNAUTHENTICATED",
);
}
if (status === 403) {
await response.body?.cancel().catch(() => {});
return settled(
forbidden(certaintyForAbandonedAttempt(attemptState, isCommand), isCommand),
"FORBIDDEN",
);
}
if (status === 429) {
await response.body?.cancel().catch(() => {});
return Object.freeze({
result: Object.freeze({
kind: "RATE_LIMITED" as const,
...(retryAfterMs === null ? {} : { retryAfterMs }),
effect: normalizeOptionalEffect(
certaintyForAbandonedAttempt(attemptState, isCommand),
isCommand,
),
}),
certainty: "RATE_LIMITED",
retryHint: true,
retryAfterMs,
});
}
return admitProblem(
operation,
response,
status,
metadata,
attemptState,
readResponseBytes,
);
}
// Success status: body policy first.
if (contract.responseBody === "NONE") {
const probe = await probeForbiddenBody(response);
if (!probe.ok) {
return settled(
transportFailure(
"RESPONSE_STREAM_FAILURE",
false,
attemptState,
isCommand,
),
probe.code,
);
}
if (probe.present) {
return settled(
violation("UNEXPECTED_BODY", "RESPONSE", postDispatchEffect(isCommand)),
"CONTRACT_VIOLATION",
);
}
if (!context.scope.isCurrent()) {
return settled(
scopeFenced(postDispatchEffect(isCommand)),
"SCOPE_FENCED",
);
}
return settled(
Object.freeze({
kind: "SUCCESS" as const,
value: undefined as WireOutput,
metadata,
effect: (isCommand
? "APPLIED_CONFIRMED"
: "NOT_APPLICABLE") as "NOT_APPLICABLE" | "APPLIED_CONFIRMED",
}),
isCommand ? "APPLIED_CONFIRMED" : "NOT_APPLICABLE",
);
}
const emptyAllowed = contract.emptyBodyStatuses.includes(status);
const mediaOk = isJsonMediaType(response.headers.get("content-type"));
const bytes = await readResponseBytes(response, policy.responseByteLimit);
if (!bytes.ok) {
return settled(
bytes.code === "RESPONSE_TOO_LARGE"
? violation(
"RESPONSE_TOO_LARGE",
"RESPONSE",
postDispatchEffect(isCommand),
)
: transportFailure(
"RESPONSE_STREAM_FAILURE",
false,
attemptState,
isCommand,
),
bytes.code,
);
}
const empty = isEffectivelyEmpty(bytes.bytes);
if (empty) {
if (contract.responseBody === "REQUIRED_JSON" || !emptyAllowed) {
return settled(
violation(
"UNEXPECTED_EMPTY_BODY",
"RESPONSE",
postDispatchEffect(isCommand),
),
"CONTRACT_VIOLATION",
);
}
if (!context.scope.isCurrent()) {
return settled(
scopeFenced(postDispatchEffect(isCommand)),
"SCOPE_FENCED",
);
}
return settled(
Object.freeze({
kind: "SUCCESS" as const,
value: undefined as WireOutput,
metadata,
effect: (isCommand
? "APPLIED_CONFIRMED"
: "NOT_APPLICABLE") as "NOT_APPLICABLE" | "APPLIED_CONFIRMED",
}),
isCommand ? "APPLIED_CONFIRMED" : "NOT_APPLICABLE",
);
}
if (!mediaOk) {
return settled(
violation(
"CONTENT_TYPE_MISMATCH",
"RESPONSE",
postDispatchEffect(isCommand),
),
"CONTRACT_VIOLATION",
);
}
const decoded = decodeJsonBytes(bytes.bytes);
if (!decoded.ok) {
return settled(
violation(decoded.code, "RESPONSE", postDispatchEffect(isCommand)),
"CONTRACT_VIOLATION",
);
}
const validated = invokeValidator(contract.outputValidator, decoded.value);
if (validated.outcome === "THROWN") {
return settled(
violation(
"VALIDATOR_RUNTIME_FAILURE",
"VALIDATION",
postDispatchEffect(isCommand),
),
"CONTRACT_VIOLATION",
);
}
if (validated.outcome === "INVALID") {
return settled(
violation(
"SUCCESS_SCHEMA_INVALID",
"VALIDATION",
postDispatchEffect(isCommand),
),
"CONTRACT_VIOLATION",
);
}
// §10.8. A successful response for a stale scope is discarded, not committed.
if (!context.scope.isCurrent()) {
return settled(
scopeFenced(postDispatchEffect(isCommand)),
"SCOPE_FENCED",
);
}
return settled(
Object.freeze({
kind: "SUCCESS" as const,
value: validated.value,
metadata,
effect: (isCommand
? "APPLIED_CONFIRMED"
: "NOT_APPLICABLE") as "NOT_APPLICABLE" | "APPLIED_CONFIRMED",
}),
isCommand ? "APPLIED_CONFIRMED" : "NOT_APPLICABLE",
);
}
const ABORTED = Symbol("http-operation-aborted");
async function awaitWithAbort<Value>(
operation: Promise<Value>,
signal: AbortSignal,
): Promise<Value | typeof ABORTED> {
if (signal.aborted) return ABORTED;
let onAbort: (() => void) | undefined;
const aborted = new Promise<typeof ABORTED>((resolve) => {
onAbort = () => resolve(ABORTED);
signal.addEventListener("abort", onAbort, { once: true });
});
try {
return await Promise.race([operation, aborted]);
} finally {
if (onAbort) signal.removeEventListener("abort", onAbort);
}
}
async function admitProblem<Input, WireOutput, Problem>(
operation: InstalledHttpContract<Input, WireOutput, Problem>,
response: Response,
status: number,
metadata: SafeResponseMetadata,
attemptState: PhysicalAttemptState,
readResponseBytes: typeof readBoundedBytes,
): Promise<AdmissionOutcome<WireOutput, Problem>> {
const contract = operation.contract;
const isCommand = contract.commandEffect !== null;
const retryable = RETRYABLE_STATUSES.has(status);
const bytes = await readResponseBytes(
response,
HTTP_EXECUTION_CEILINGS.problemResponseBytes,
);
if (!bytes.ok || isEffectivelyEmpty(bytes.bytes)) {
// An unclassifiable failure stays uncertain for a command.
return Object.freeze({
result: violation<WireOutput, Problem>(
"PROBLEM_SCHEMA_INVALID",
"VALIDATION",
postDispatchEffect(isCommand),
),
certainty: isCommand ? "MAYBE_APPLIED" : "NOT_STARTED",
retryHint: retryable,
retryAfterMs: metadata.retryAfterMs ?? null,
});
}
if (!isJsonMediaType(response.headers.get("content-type"))) {
return Object.freeze({
result: violation<WireOutput, Problem>(
"CONTENT_TYPE_MISMATCH",
"RESPONSE",
postDispatchEffect(isCommand),
),
certainty: isCommand ? "MAYBE_APPLIED" : "NOT_STARTED",
retryHint: retryable,
retryAfterMs: metadata.retryAfterMs ?? null,
});
}
const decoded = decodeJsonBytes(bytes.bytes);
if (!decoded.ok) {
return Object.freeze({
result: violation<WireOutput, Problem>(
decoded.code,
"RESPONSE",
postDispatchEffect(isCommand),
),
certainty: isCommand ? "MAYBE_APPLIED" : "NOT_STARTED",
retryHint: retryable,
retryAfterMs: metadata.retryAfterMs ?? null,
});
}
const validated = invokeValidator(contract.problemValidator, decoded.value);
if (validated.outcome !== "VALID") {
return Object.freeze({
result: violation<WireOutput, Problem>(
validated.outcome === "THROWN"
? "VALIDATOR_RUNTIME_FAILURE"
: "PROBLEM_SCHEMA_INVALID",
"VALIDATION",
postDispatchEffect(isCommand),
),
certainty: isCommand ? "MAYBE_APPLIED" : "NOT_STARTED",
retryHint: retryable,
retryAfterMs: metadata.retryAfterMs ?? null,
});
}
const classification = classifyProblemEffect({
status,
problem: validated.value,
descriptor: contract.commandEffect,
});
const effect = isCommand
? classification.effect
: certaintyForAbandonedAttempt(attemptState, false);
return Object.freeze({
result: Object.freeze({
kind: "PROBLEM" as const,
problem: validated.value,
metadata,
effect: (effect === "NOT_STARTED"
? "NOT_APPLIED"
: effect) as "NOT_APPLIED" | "APPLIED_CONFIRMED" | "MAYBE_APPLIED",
}),
certainty: effect,
retryHint: retryable && effect !== "MAYBE_APPLIED",
retryAfterMs: metadata.retryAfterMs ?? null,
});
}
/**
* §8.3. `SAFE` and `IDEMPOTENT` may replay the same frozen request. `KEYED`
* must not automatically retry once an attempt was dispatched and its response
* was lost; that path goes to inspect/reconciliation instead. `NEVER` is zero.
*/
function canRetryTransport(
semantics: InstalledHttpContract<
unknown,
unknown,
unknown
>["contract"]["retrySemantics"],
attemptState: PhysicalAttemptState,
): boolean {
if (semantics === "NEVER") return false;
if (semantics === "KEYED") {
return attemptState === "PREPARING" || attemptState === "READY_TO_SEND";
}
return true;
}
function isRetryableSemantics(
semantics: InstalledHttpContract<
unknown,
unknown,
unknown
>["contract"]["retrySemantics"],
): boolean {
return semantics === "SAFE" || semantics === "IDEMPOTENT";
}
/** §8.2. Full jitter over `min(2000, 250 * 2^index)`. */
function jitteredDelay(retryIndex: number, random: () => number): number {
const ceiling = Math.min(
RETRY_MAX_LOCAL_DELAY_MS,
RETRY_BASE_DELAY_MS * 2 ** retryIndex,
);
return Math.floor(random() * ceiling);
}
function retryDelayFor(
retryAfterMs: number | null,
retryIndex: number,
random: () => number,
): number | null {
const local = jitteredDelay(retryIndex, random);
if (retryAfterMs === null) return local;
if (retryAfterMs > RETRY_AFTER_CEILING_MS) return null;
return Math.max(local, retryAfterMs);
}
/** §7.12. No raw header map, URL, cookie, traceparent or ETag value escapes. */
function safeMetadata(
response: Response,
retryAfterMs: number | null,
): SafeResponseMetadata {
const correlation = response.headers.get("x-correlation-id");
return Object.freeze({
status: response.status,
...(correlation && /^[A-Za-z0-9._:-]{1,128}$/.test(correlation)
? { correlationReference: correlation }
: {}),
...(retryAfterMs === null ? {} : { retryAfterMs }),
});
}
function settled<Value, Problem>(
result: HttpExecutionOutcome<Value, Problem>,
certainty: string,
): AdmissionOutcome<Value, Problem> {
return Object.freeze({
result,
certainty,
retryHint: false,
retryAfterMs: null,
});
}
function violation<Value, Problem>(
kind: HttpContractViolationKind,
operation: HttpContractViolation["operation"],
effect: HttpEffectCertainty,
): HttpExecutionOutcome<Value, Problem> {
return Object.freeze({
kind: "CONTRACT_VIOLATION" as const,
violation: Object.freeze({ kind, operation }),
effect,
});
}
function scopeFenced<Value, Problem>(
effect: HttpEffectCertainty,
): HttpExecutionOutcome<Value, Problem> {
return violation("SCOPE_FENCED", "RESPONSE", effect);
}
function preDispatchEffect(isCommand: boolean): HttpEffectCertainty {
return isCommand ? "NOT_STARTED" : "NOT_APPLICABLE";
}
function postDispatchEffect(isCommand: boolean): HttpEffectCertainty {
return isCommand ? "MAYBE_APPLIED" : "NOT_APPLICABLE";
}
function contractViolationEffect(
attemptState: PhysicalAttemptState,
isCommand: boolean,
): HttpEffectCertainty {
if (!isCommand) return "NOT_APPLICABLE";
return attemptState === "PREPARING" || attemptState === "READY_TO_SEND"
? "NOT_STARTED"
: "MAYBE_APPLIED";
}
function unauthenticated<Value, Problem>(
effect: string,
isCommand: boolean,
): HttpExecutionOutcome<Value, Problem> {
return Object.freeze({
kind: "UNAUTHENTICATED" as const,
effect: normalizeOptionalEffect(effect, isCommand),
});
}
function forbidden<Value, Problem>(
effect: string,
isCommand: boolean,
): HttpExecutionOutcome<Value, Problem> {
return Object.freeze({
kind: "FORBIDDEN" as const,
effect: normalizeOptionalEffect(effect, isCommand),
});
}
function cancelled<Value, Problem>(
effect: "NOT_STARTED" | "NOT_APPLIED" | "MAYBE_APPLIED" | "APPLIED_CONFIRMED",
): HttpExecutionOutcome<Value, Problem> {
return Object.freeze({
kind: "CANCELLED" as const,
effect: effect === "MAYBE_APPLIED" ? "MAYBE_APPLIED" : "NOT_STARTED",
});
}
function transportFailure<Value, Problem>(
kind: HttpTransportFailure["kind"],
retryable: boolean,
attemptState: PhysicalAttemptState,
isCommand: boolean,
): HttpExecutionOutcome<Value, Problem> {
const effect = certaintyForAbandonedAttempt(attemptState, isCommand);
return Object.freeze({
kind: "TRANSPORT_FAILURE" as const,
failure: Object.freeze({ kind, retryable }),
effect: effect === "MAYBE_APPLIED" ? "MAYBE_APPLIED" : "NOT_STARTED",
});
}
function normalizeOptionalEffect(
effect: string,
isCommand: boolean,
): "NOT_APPLICABLE" | "NOT_APPLIED" | "MAYBE_APPLIED" {
if (!isCommand) return "NOT_APPLICABLE";
return effect === "MAYBE_APPLIED" ? "MAYBE_APPLIED" : "NOT_APPLIED";
}