# @gyral/http

> Gyral HTTP driver: fetch requests as data, typed errors, Standard Schema decoding.

Source: https://gyral.dev/docs/api/http/

Gyral HTTP driver: fetch requests as data, typed errors, Standard Schema decoding.

```sh
npm install @gyral/http
```

> **Writing code? Start from working code, not from this page.** [Effects and drivers](https://gyral.dev/docs/effects/). 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`](#gyralhttp), [`@gyral/http/sse`](#gyralhttpsse), [`@gyral/http/websocket`](#gyralhttpwebsocket), [`@gyral/http/testing`](#gyralhttptesting).

## `@gyral/http`

### Functions

| Name | Summary |
| --- | --- |
| [`csrfFromMeta`](#http-csrffrommeta) | A header source that reads a token from `<meta name=…>` when each request runs. |
| [`get`](#http-get) |  |
| [`makeHttpDriver`](#http-makehttpdriver) |  |
| [`redirectedTo`](#http-redirectedto) | The `location` of a `FormRedirected` answer (`formAction` in `@gyral/ssr`), if it is one. |
| [`request`](#http-request) | A command that performs `req` with the `http` driver (or its substitute). |
| [`retryableHttpError`](#http-retryablehttperror) | The failures worth retrying: network errors, timeouts, 408, 429 and 5xx. |
| [`submitForm`](#http-submitform) | Sends a form submission to the server (usually after `form()` validated it in the browser, which passes the raw `FormData`). |

#### `csrfFromMeta`

```ts
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`

```ts
function get<O = unknown, MS = never, MF = MS>(url: string, handlers: RequestHandlers<O, MS, MF>): Command<MS | MF>;
```


#### `makeHttpDriver`

```ts
function makeHttpDriver(options?: HttpDriverOptions): Driver<HttpRequest, unknown, HttpError>;
```


#### `redirectedTo`

```ts
function redirectedTo(body: unknown): string | undefined;
```

The `location` of a `FormRedirected` answer (`formAction` in `@gyral/ssr`), if it is one.

#### `request`

```ts
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`

```ts
const retryableHttpError: (error: HttpError) => boolean
```

The 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`

```ts
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`.


```ts
Register: (s, m) => [{ ...s, busy: true }, [submitForm('/register', m.form, {
  onSuccess: (body) => ({ _tag: 'Registered', location: redirectedTo(body) }),
  onFailure: () => ({ _tag: 'Failed' }),
})]],
```


### Constants

| Name | Summary |
| --- | --- |
| [`http`](#http-http) | The default driver. |

#### `http`

```ts
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`](#http-formredirected) | A server's JSON answer to a valid form post that would have redirected without JS. |
| [`HeaderSource`](#http-headersource) | Headers as data, or computed when each request runs (e.g. |
| [`HttpDriverOptions`](#http-httpdriveroptions) |  |
| [`HttpError`](#http-httperror) | Every way a request can fail. |
| [`HttpMethod`](#http-httpmethod) |  |
| [`HttpRequest`](#http-httprequest) | An HTTP request described as data. |
| [`RequestHandlers`](#http-requesthandlers) |  |
| [`SubmitFormOptions`](#http-submitformoptions) |  |

#### `FormRedirected`

```ts
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`

```ts
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`

```ts
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`

```ts
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`

```ts
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
```


#### `HttpRequest`

```ts
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`

```ts
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`

```ts
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`](#http-sse-makeeventsourcedriver) | A cancellable feed; native EventSource owns reconnection, credentials and Last-Event-ID. |

#### `makeEventSourceDriver`

```ts
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`](#http-sse-eventsourceerror) |  |
| [`EventSourceInput`](#http-sse-eventsourceinput) |  |
| [`EventSourceOptions`](#http-sse-eventsourceoptions) |  |
| [`EventSourceOutput`](#http-sse-eventsourceoutput) |  |

#### `EventSourceError`

```ts
interface EventSourceError {
    readonly _tag: 'EventSourceError';
    readonly url: string | undefined;
    readonly message: string;
}
```


#### `EventSourceInput`

```ts
interface EventSourceInput {
    readonly url: string;
}
```


#### `EventSourceOptions`

```ts
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`

```ts
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`](#http-websocket-makewebsocketdriver) | Typed receive/send/close commands on one owned socket, with bounded outgoing buffering. |

#### `makeWebSocketDriver`

```ts
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`](#http-websocket-websocketclosed) |  |
| [`WebSocketError`](#http-websocket-websocketerror) |  |
| [`WebSocketIncoming`](#http-websocket-websocketincoming) |  |
| [`WebSocketInput`](#http-websocket-websocketinput) |  |
| [`WebSocketOptions`](#http-websocket-websocketoptions) |  |
| [`WebSocketOutput`](#http-websocket-websocketoutput) |  |
| [`WebSocketPayload`](#http-websocket-websocketpayload) |  |

#### `WebSocketClosed`

```ts
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`

```ts
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`

```ts
type WebSocketIncoming = string | ArrayBuffer;
```


#### `WebSocketInput`

```ts
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`

```ts
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`

```ts
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`

```ts
type WebSocketPayload = string | Blob | ArrayBuffer | ArrayBufferView<ArrayBuffer>;
```


## `@gyral/http/testing`

### Functions

| Name | Summary |
| --- | --- |
| [`fakeHttp`](#http-testing-fakehttp) | `el.drivers = { http: fakeHttp() }` (or `provideDrivers(root, { http })`), then `http.respondNext({ body })` or `http.reply(422, problem)`. |

#### `fakeHttp`

```ts
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`](#http-testing-fakehttprespondererror) | The error a fake request fails with when the `respond` option threw. |

#### `FakeHttpResponderError`

```ts
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`](#http-testing-fakehttp) |  |
| [`FakeHttpCall`](#http-testing-fakehttpcall) | One request the fake driver received. |
| [`FakeHttpOptions`](#http-testing-fakehttpoptions) |  |
| [`FakeResponse`](#http-testing-fakeresponse) | What a fake server answers: a status (default 200) and a JSON body (omit for none). |

#### `FakeHttp`

```ts
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`

```ts
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`

```ts
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`

```ts
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).
