@safe-shape/http provides framework-neutral helpers for validating HTTP boundary data
with @safe-shape/core schemas.
Use httpContract(config).
Supported sections:
paramsquerybodyheaderscookiesresponseresponses
Each section accepts a core schema.
Contracts expose:
safeParseRequest(input)parseRequest(input)safeParseRequestAsync(input)parseRequestAsync(input)
safeParseRequest returns a core ParseResult.
Standalone helpers are also available for adapter-style code:
safeParseHttpRequest(contract, input)parseHttpRequest(contract, input)safeParseHttpRequestAsync(contract, input)parseHttpRequestAsync(contract, input)
Request failures prefix issue paths with the section name:
input.body.email
input.headers.authorization
input.cookies.sessionFor failed ordinary unions, section prefixes are applied recursively to every
issue in the preserved branches tree as well as to the root issue.
Addressable custom diagnostics keep collector order and receive the same
section prefix. For example, a body refinement issue with relative path
["end"] is returned at ["body", "end"].
Warnings use the same prefix and remain non-fatal.
Contracts expose:
safeParseResponse(input, status?)parseResponse(input, status?)safeParseResponseAsync(input, status?)parseResponseAsync(input, status?)
Standalone helpers are also available:
safeParseHttpResponse(contract, input, status?)parseHttpResponse(contract, input, status?)safeParseHttpResponseAsync(contract, input, status?)parseHttpResponseAsync(contract, input, status?)
If no response schema is configured, response parsing returns the input unchanged.
Use responses for status-specific response schemas:
const contract = httpContract({
responses: {
200: object({ id: string() }),
404: object({ message: string() }),
},
});
const response = contract.parseResponse({ id: "user_1" }, 200);If status is provided and no status schema exists, response is used as a fallback
when configured. Without a fallback response schema, parsing fails at input.response.status.
recoverHttpResponse() (or recoverHttpResponseAsync() for async contracts)
validates a network value and, only after failure, an
eager or lazy fallback through the same contract and status:
const state = recoverHttpResponse(contract, networkPayload, {
status: 200,
getFallback: () => readCachedValue(),
});
switch (state.kind) {
case "valid":
case "recovered":
render(state.data);
break;
case "unavailable":
renderUnavailable();
}Use fallback: unknown for an eager value or getFallback: () => unknown for
a lazy value. Exactly one is required. A valid network response never invokes
getFallback. recovered retains networkError; unavailable retains both
networkError and fallbackError. All states are frozen, and neither invalid
payload is exposed as inferred response data.
The returned valid value's warnings are preserved on valid and recovered
states.
Fallback callbacks are application code; thrown exceptions propagate. Catch
fallible cache/storage work inside the callback and return an unknown value
for validation. Telemetry, redaction, retries, and UI policy remain outside the
helper.
See Production response recovery for a typed pattern, a runnable example, and telemetry safety guidance.
@safe-shape/http remains a runtime-only package. For contract evolution,
compare the relevant request or response schema with @safe-shape/compat, then
create an HTTP-specific presentation:
import {
compareContractsV2,
createHttpCompatibilityPresentation,
} from "@safe-shape/compat";
const report = compareContractsV2(previousResponse, nextResponse, {
compatibility: "forward",
});
const presentation = createHttpCompatibilityPresentation(report, {
exchange: "response",
});Request producers are clients and request consumers are servers. Response producers are servers and response consumers are clients. Backward containment is presented as consumer compatibility; forward containment is presented as producer compatibility. Full mode covers both roles.
import { object, string } from "@safe-shape/core";
import { httpContract, parseHttpRequest } from "@safe-shape/http";
const contract = httpContract({
params: object({ id: string() }),
body: object({ name: string() }),
headers: object({ authorization: string() }),
cookies: object({ session: string() }),
response: object({ id: string() }),
responses: {
404: object({ message: string() }),
},
});
const request = contract.parseRequest({
params: { id: "user_1" },
body: { name: "Dev" },
headers: { authorization: "Bearer token" },
cookies: { session: "session_1" },
});
const sameRequest = parseHttpRequest(contract, {
params: { id: "user_1" },
body: { name: "Dev" },
headers: { authorization: "Bearer token" },
cookies: { session: "session_1" },
});