@gyral/http
Gyral HTTP driver: fetch requests as data, typed errors, Standard Schema decoding.
npm install @gyral/httpWriting code? Start from working code, not from this page. Effects and drivers. Adapt the closest example and check it, then look a name up here when you are about to call it.
Generated from the package's type declarations. Entry points: @gyral/http, @gyral/http/sse, @gyral/http/websocket, @gyral/http/testing.
@gyral/http
Functions
| Name | Summary |
|---|---|
csrfFromMeta |
A header source that reads a token from <meta name=…> when each request runs. |
get |
|
makeHttpDriver |
|
redirectedTo |
The location of a FormRedirected answer (formAction in @gyral/ssr), if it is one. |
request |
A command that performs req with the http driver (or its substitute). |
retryableHttpError |
The failures worth retrying: network errors, timeouts, 408, 429 and 5xx. |
submitForm |
Sends a form submission to the server (usually after form() validated it in the browser, which passes the raw FormData). |
csrfFromMeta
function csrfFromMeta(meta: string, header?: string): () => Readonly<Record<string, string>>;A header source that reads a token from <meta name=…> when each request runs. On the
server, or when the meta is missing, it adds nothing. The header defaults to x-csrf-token.
get
function get<O = unknown, MS = never, MF = MS>(url: string, handlers: RequestHandlers<O, MS, MF>): Command<MS | MF>;makeHttpDriver
function makeHttpDriver(options?: HttpDriverOptions): Driver<HttpRequest, unknown, HttpError>;redirectedTo
function redirectedTo(body: unknown): string | undefined;The location of a FormRedirected answer (formAction in @gyral/ssr), if it is one.
request
function request<O = unknown, MS = never, MF = MS>(req: Omit<HttpRequest, 'schema' | 'errorSchema'>, handlers: RequestHandlers<O, MS, MF>): Command<MS | MF>;A command that performs req with the http driver (or its substitute).
retryableHttpError
const retryableHttpError: (error: HttpError) => booleanThe failures worth retrying: network errors, timeouts, 408, 429 and 5xx. Pass it as
retry(…, { retryIf: retryableHttpError }). A 4xx other than 408/429 means the request
itself is wrong, and decoding errors won't change on a retry. Retry-After isn't read.
submitForm
function submitForm<MS, MF = MS>(url: string, formData: FormData, options: SubmitFormOptions<MS, MF>): Command<MS | MF | IntentRejected>;Sends a form submission to the server (usually after form() validated it in the browser,
which passes the raw FormData). The body is the FormData itself, so the server's
formAction parses it exactly like a no-JS post. A 422 answer with an IntentRejected
(server-only checks: duplicate email, wrong password) is dispatched as IntentRejected.
Register: (s, m) => [{ ...s, busy: true }, [submitForm('/register', m.form, {
onSuccess: (body) => ({ _tag: 'Registered', location: redirectedTo(body) }),
onFailure: () => ({ _tag: 'Failed' }),
})]],Constants
| Name | Summary |
|---|---|
http |
The default driver. |
http
const http: Driver<HttpRequest, unknown, HttpError>The default driver. Substitute it by name: el.drivers = { http: makeHttpDriver({...}) }.
Types and interfaces
You rarely write most of these yourself: TypeScript infers them from the functions above. Look one up when an example or a compiler error names it.
| Name | Summary |
|---|---|
FormRedirected |
A server's JSON answer to a valid form post that would have redirected without JS. |
HeaderSource |
Headers as data, or computed when each request runs (e.g. |
HttpDriverOptions |
|
HttpError |
Every way a request can fail. |
HttpMethod |
|
HttpRequest |
An HTTP request described as data. |
RequestHandlers |
|
SubmitFormOptions |
FormRedirected
interface FormRedirected {
readonly _tag: 'Redirected';
readonly location: string;
}A server's JSON answer to a valid form post that would have redirected without JS.
HeaderSource
type HeaderSource = Readonly<Record<string, string>> | ((req: HttpRequest) => Readonly<Record<string, string>> | undefined);Headers as data, or computed when each request runs (e.g. read a CSRF <meta>).
HttpDriverOptions
interface HttpDriverOptions {
/** Driver name used for substitution (`el.drivers`). Default `'http'`. */
readonly name?: string;
/**
* Default headers for every request through this driver: app-level concerns such as a CSRF
* token or an API key, so components and stores never read the DOM themselves. This is the
* one place a CSRF token is configured (ADR 0022). Per-request `headers` win over these.
*
* provideDrivers(document.body, { http: makeHttpDriver({ headers: csrfFromMeta('csrf-token') }) });
*/
readonly headers?: HeaderSource;
/** Resolves relative URLs. Default: the document location. */
readonly baseUrl?: string;
readonly fetch?: typeof fetch;
readonly concurrency?: Concurrency;
/**
* Aborts a request that takes longer than this, failing with `HttpTimeoutError`. Applies per
* attempt: under `retry(…)` each attempt gets the full time. Default: no limit.
*/
readonly timeoutMs?: number;
}HttpError
type HttpError = {
readonly _tag: 'HttpStatusError';
readonly url: string;
readonly status: number;
readonly statusText: string;
/** The response body: parsed JSON, or text, or `undefined` when empty. */
readonly body: unknown;
/** `body` decoded by the request's `errorSchema`, when it was given and matched. */
readonly detail?: unknown;
} | {
readonly _tag: 'HttpNetworkError';
readonly url: string;
readonly message: string;
}
/** The attempt took longer than the driver's `timeoutMs`. */
| {
readonly _tag: 'HttpTimeoutError';
readonly url: string;
readonly timeoutMs: number;
} | {
readonly _tag: 'HttpDecodeError';
readonly url: string;
readonly issues: ReadonlyArray<StandardSchemaV1.Issue>;
};Every way a request can fail. Delivered to onFailure, never thrown into the view.
HttpMethod
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';HttpRequest
interface HttpRequest {
readonly url: string;
readonly method?: HttpMethod;
readonly headers?: Readonly<Record<string, string>>;
/** Sent as JSON, except `FormData` and `URLSearchParams`, which are sent as forms. */
readonly body?: unknown;
/** Decodes the JSON response. Any Standard Schema library works (Zod, Valibot, Effect…). */
readonly schema?: StandardSchemaV1;
/**
* Decodes the body of a non-2xx response into `HttpStatusError.detail` (for example a 422
* `IntentRejected`). A body that doesn't match leaves `detail` unset; `body` is always kept.
*/
readonly errorSchema?: StandardSchemaV1;
}An HTTP request described as data.
RequestHandlers
interface RequestHandlers<O, MS, MF> {
/** Decodes the response; its output type becomes `onSuccess`'s input. */
readonly schema?: StandardSchemaV1<unknown, O>;
/** Decodes non-2xx bodies into `HttpStatusError.detail` (e.g. a 422 `IntentRejected`). */
readonly errorSchema?: StandardSchemaV1;
readonly onSuccess: (body: O) => MS | undefined;
readonly onFailure?: (error: HttpError) => MF | undefined;
/** Concurrency lane, e.g. `'search'`. Default `'http'`. */
readonly key?: string;
readonly concurrency?: Concurrency;
}SubmitFormOptions
interface SubmitFormOptions<MS, MF> {
readonly method?: 'POST' | 'PUT' | 'PATCH';
/**
* A CSRF token the app already holds, for this submission only. The header defaults to
* `x-csrf-token`. A token from a `<meta>` is configured once, on the driver:
* `makeHttpDriver({ headers: csrfFromMeta('csrf-token') })` (ADR 0022).
*/
readonly csrf?: {
readonly token: string;
readonly header?: string;
};
/** The server's success answer, e.g. `{ _tag: 'Redirected', location }` (see `redirectedTo`). */
readonly onSuccess: (body: unknown) => MS | undefined;
/** Any failure except a 422 `IntentRejected`, which goes to the `IntentRejected` reducer. */
readonly onFailure?: (error: HttpError) => MF | undefined;
/** Concurrency lane. Default `form:<url>`. */
readonly key?: string;
/** Default `exhaust`: a second submit while one is in flight is ignored. */
readonly concurrency?: Concurrency;
}@gyral/http/sse
Functions
| Name | Summary |
|---|---|
makeEventSourceDriver |
A cancellable feed; native EventSource owns reconnection, credentials and Last-Event-ID. |
makeEventSourceDriver
function makeEventSourceDriver<T>(schema: StandardSchemaV1<unknown, T>, options?: EventSourceOptions): Driver<EventSourceInput, EventSourceOutput<T>, EventSourceError>;A cancellable feed; native EventSource owns reconnection, credentials and Last-Event-ID.
Types and interfaces
You rarely write most of these yourself: TypeScript infers them from the functions above. Look one up when an example or a compiler error names it.
| Name | Summary |
|---|---|
EventSourceError |
|
EventSourceInput |
|
EventSourceOptions |
|
EventSourceOutput |
EventSourceError
interface EventSourceError {
readonly _tag: 'EventSourceError';
readonly url: string | undefined;
readonly message: string;
}EventSourceInput
interface EventSourceInput {
readonly url: string;
}EventSourceOptions
interface EventSourceOptions {
readonly name?: string;
readonly withCredentials?: boolean;
/** Named events in addition to the default message event. No wildcard native listener. */
readonly events?: readonly string[];
/** Default JSON parsing. Validation may be asynchronous. */
readonly parse?: (text: string) => unknown;
}EventSourceOutput
type EventSourceOutput<T> = {
readonly _tag: 'EventSourceState';
readonly url: string;
readonly state: 'connecting' | 'open' | 'closed';
} | {
readonly _tag: 'EventSourceMessage';
readonly url: string;
readonly eventType: string;
readonly lastEventId: string;
readonly data: T;
} | {
readonly _tag: 'EventSourceDecodeFailed';
readonly url: string;
readonly eventType: string;
readonly lastEventId: string;
readonly issues: readonly StandardSchemaV1.Issue[];
};@gyral/http/websocket
Functions
| Name | Summary |
|---|---|
makeWebSocketDriver |
Typed receive/send/close commands on one owned socket, with bounded outgoing buffering. |
makeWebSocketDriver
function makeWebSocketDriver<T, Out>(schema: StandardSchemaV1<unknown, T>, options: WebSocketOptions<Out>): Driver<WebSocketInput<Out>, WebSocketOutput<T>, WebSocketError>;Typed receive/send/close commands on one owned socket, with bounded outgoing buffering.
Types and interfaces
You rarely write most of these yourself: TypeScript infers them from the functions above. Look one up when an example or a compiler error names it.
| Name | Summary |
|---|---|
WebSocketClosed |
|
WebSocketError |
|
WebSocketIncoming |
|
WebSocketInput |
|
WebSocketOptions |
|
WebSocketOutput |
|
WebSocketPayload |
WebSocketClosed
interface WebSocketClosed {
readonly _tag: 'WebSocketClosed';
readonly connection: string;
readonly url: string;
readonly code: number;
readonly reason: string;
readonly wasClean: boolean;
/** The application issued a valid Close command on this connection. */
readonly requested: boolean;
}WebSocketError
type WebSocketError = {
readonly _tag: 'WebSocketConnectError';
readonly connection: string | undefined;
readonly url: string | undefined;
readonly message: string;
} | {
readonly _tag: 'WebSocketClosedError';
readonly connection: string;
readonly url: string;
readonly code: number;
readonly reason: string;
readonly wasClean: boolean;
readonly errored: boolean;
readonly requested: boolean;
} | {
readonly _tag: 'WebSocketSendError';
readonly connection: string;
readonly reason: 'not-open' | 'buffer-limit' | 'encode' | 'send';
readonly message: string;
readonly bufferedBytes?: number;
readonly bytes?: number;
readonly limit?: number;
} | {
readonly _tag: 'WebSocketCloseError';
readonly connection: string;
readonly message: string;
};WebSocketIncoming
type WebSocketIncoming = string | ArrayBuffer;WebSocketInput
type WebSocketInput<T> = {
readonly _tag: 'Connect';
readonly connection: string;
readonly url: string;
readonly protocols?: string | readonly string[];
} | {
readonly _tag: 'Send';
readonly connection: string;
readonly value: T;
} | {
readonly _tag: 'Close';
readonly connection: string;
readonly code?: number;
readonly reason?: string;
};WebSocketOptions
interface WebSocketOptions<T> {
readonly name?: string;
readonly encode: (value: T) => WebSocketPayload;
/** Synchronous parse; default JSON text. Validation may be async. */
readonly parse?: (payload: WebSocketIncoming) => unknown;
/** Maximum native bufferedAmount + encoded payload bytes. Default 65536. */
readonly maxBufferedBytes?: number;
}WebSocketOutput
type WebSocketOutput<T> = {
readonly _tag: 'WebSocketState';
readonly connection: string;
readonly url: string;
readonly state: 'connecting' | 'open' | 'closing';
} | {
readonly _tag: 'WebSocketMessage';
readonly connection: string;
readonly data: T;
} | {
readonly _tag: 'WebSocketDecodeFailed';
readonly connection: string;
readonly issues: readonly StandardSchemaV1.Issue[];
} | {
readonly _tag: 'WebSocketSent';
readonly connection: string;
readonly bytes: number;
} | {
readonly _tag: 'WebSocketCloseRequested';
readonly connection: string;
} | WebSocketClosed;WebSocketPayload
type WebSocketPayload = string | Blob | ArrayBuffer | ArrayBufferView<ArrayBuffer>;@gyral/http/testing
Functions
| Name | Summary |
|---|---|
fakeHttp |
el.drivers = { http: fakeHttp() } (or provideDrivers(root, { http })), then http.respondNext({ body }) or http.reply(422, problem). |
fakeHttp
function fakeHttp(options?: FakeHttpOptions): FakeHttp;el.drivers = { http: fakeHttp() } (or provideDrivers(root, { http })), then
http.respondNext({ body }) or http.reply(422, problem). Request inputs are recorded in
requests (alias inputs).
Classes
| Name | Summary |
|---|---|
FakeHttpResponderError |
The error a fake request fails with when the respond option threw. |
FakeHttpResponderError
class FakeHttpResponderError extends Error {
readonly request: HttpRequest;
constructor(request: HttpRequest, cause: unknown);
}The error a fake request fails with when the respond option threw.
Types and interfaces
You rarely write most of these yourself: TypeScript infers them from the functions above. Look one up when an example or a compiler error names it.
| Name | Summary |
|---|---|
FakeHttp |
|
FakeHttpCall |
One request the fake driver received. |
FakeHttpOptions |
|
FakeResponse |
What a fake server answers: a status (default 200) and a JSON body (omit for none). |
FakeHttp
interface FakeHttp extends Driver<HttpRequest, unknown, HttpError> {
readonly calls: readonly FakeHttpCall[];
/** Every request so far. */
readonly requests: readonly HttpRequest[];
/** Same as `requests`; matches `fakeDriver(…).inputs` from `@gyral/testing`. */
readonly inputs: readonly HttpRequest[];
/** Errors thrown (or rejected) by the `respond` option, in order. */
readonly responderErrors: readonly unknown[];
/** Answers the oldest request that is still waiting. Throws if there is none. */
respondNext(response?: FakeResponse): void;
/** Shorthand for `respondNext({ status, body })`, e.g. `reply(422, problem)`. */
reply(status: number, body?: unknown): void;
/** Fails the oldest waiting request as a network error. Throws if there is none. */
failNext(message?: string): void;
}FakeHttpCall
interface FakeHttpCall {
readonly request: HttpRequest;
readonly signal: AbortSignal;
readonly settled: boolean;
/** Answers this request; the real driver then decodes it. */
readonly respond: (response?: FakeResponse) => void;
/** Fails this request as a network error (`HttpNetworkError`). */
readonly fail: (message?: string) => void;
}One request the fake driver received.
FakeHttpOptions
interface FakeHttpOptions {
/** Driver name to substitute (`el.drivers`, providers). Default `'http'`. */
readonly name?: string;
/** Answer every request immediately. Without it, requests wait for `respondNext`. */
readonly respond?: (request: HttpRequest) => FakeResponse | Promise<FakeResponse>;
/** Base URL for relative request URLs. Default `'http://localhost/'`. */
readonly baseUrl?: string;
readonly headers?: HeaderSource;
readonly concurrency?: Concurrency;
/**
* Called when the `respond` option throws or rejects. That is a bug in the test, not a
* network failure, so by default it is rethrown as an uncaught error (Vitest fails the run).
* The request itself still fails (as `HttpNetworkError`) so the component never hangs.
*/
readonly onResponderError?: (error: unknown, request: HttpRequest) => void;
}FakeResponse
interface FakeResponse {
readonly status?: number;
readonly statusText?: string;
readonly body?: unknown;
}What a fake server answers: a status (default 200) and a JSON body (omit for none).