Skip to content

Latest commit

 

History

History
183 lines (140 loc) · 5.09 KB

File metadata and controls

183 lines (140 loc) · 5.09 KB

HTTP API

@safe-shape/http provides framework-neutral helpers for validating HTTP boundary data with @safe-shape/core schemas.

Contract

Use httpContract(config).

Supported sections:

  • params
  • query
  • body
  • headers
  • cookies
  • response
  • responses

Each section accepts a core schema.

Request Parsing

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

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

Response Parsing

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.

Production Response Recovery

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.

Compatibility Presentation

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

Example

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" },
});