# jg-fetch

> A fetch wrapper that never throws: timeouts, retries that honour Retry-After, response size caps, and API envelopes.

- **Version:** `0.3.0`
- **Runtimes:** Universal
- **Install:** `npm install jg-fetch`
- **On npm:** [jg-fetch](https://www.npmjs.com/package/jg-fetch)
- **Source:** [packages/jg-fetch](https://github.com/jesusgraterol/jg-packages/tree/main/packages/jg-fetch)
- **Recipes:** [jg-fetch recipes](https://jg-packages.jesusgraterol.dev/packages/jg-fetch/recipes/index.md)

## Usage

`request` resolves to `{ data, error: null, meta }` or `{ data: null, error, meta }`, and never throws:

```ts
import { request } from 'jg-fetch';

const { data, error, meta } = await request<IUser>('https://api.example.com/users/1');
if (error) {
  error.kind; // what failed, such as 'TIMEOUT', 'NETWORK_ERROR', or 'HTTP_ERROR'
  error.status; // 404, or null when no response arrived
} else {
  data; // IUser
}
meta.attempts; // 1
```

Read a jg-api-response envelope, validate its data, and send a fresh token on every attempt:

```ts
import { request } from 'jg-fetch';
import { z } from 'zod';

const User = z.object({ id: z.number(), name: z.string() });

const { data, error } = await request('/users/me', {
  baseUrl: 'https://api.example.com',
  parseAs: 'envelope', // resolves to the envelope's data
  schema: User, // data is { id: number; name: string }
  headers: async () => ({ authorization: `Bearer ${await getIdToken()}` }),
});
if (error?.kind === 'API_ERROR') {
  error.code; // the envelope error's code, such as 'USER_NOT_FOUND'
  error.message; // the envelope error's message
  error.data; // the envelope error's data, or null without any
  error.body; // the whole envelope
}
```

When a credential can expire, such as an ID token, refresh it and repeat the attempt at once, without using up a retry:

```ts
await request('/users/me', {
  baseUrl,
  headers: async () => ({ authorization: `Bearer ${await getIdToken()}` }),
  // returning true repeats the attempt; once per call unless maxCredentialRefreshes says more
  refreshCredentials: async ({ status, signal }) => {
    if (status !== 401) {
      return false;
    }
    await refreshIdToken(signal);
    return true;
  },
});
```

Send JSON, query parameters, or any other body:

```ts
await request('/orders', {
  baseUrl,
  method: 'POST',
  body: { items: [1, 2] }, // sent as JSON, with Content-Type: application/json
  query: { notify: true, tags: ['a', 'b'] }, // ?notify=true&tags=a&tags=b
});
await request('/avatar', { baseUrl, method: 'PUT', body: formData }); // the browser sets the multipart type
```

A string, `Blob`, `ArrayBuffer` or view of one, `FormData`, `URLSearchParams`, or `ReadableStream` is sent as it is. Any other body, such as a `Map`, a `Date`, or a number, fails with `INVALID_BODY` before the request is sent, because `fetch` would turn it into text such as `[object Map]`.

Read other kinds of responses with `parseAs`: `'json'` (the default), `'envelope'`, `'text'`, `'bytes'`, `'blob'`, `'response'` for the unread `Response`, or `'none'`. The data's type follows it.

### Timeouts and retries

| Option                   | Default                                  | What it does                                                                       |
| ------------------------ | ---------------------------------------- | ---------------------------------------------------------------------------------- |
| `timeoutSeconds`         | `15`                                     | Limits the whole call, retries and waits included.                                 |
| `attemptTimeoutSeconds`  | none                                     | Limits each attempt; an attempt that runs out retries.                             |
| `retryDelaysSeconds`     | `[3, 5]`                                 | The waits before each retry. `[]` turns retries off.                               |
| `retryMethods`           | `GET`, `HEAD`, `OPTIONS`                 | The methods that may retry. Add `POST` only for requests that are safe to repeat.  |
| `retryStatusCodes`       | `408`, `429`, `500`, `502`, `503`, `504` | The statuses that may retry. `400`–`405`, `409`, `410`, `413`, and `422` never do. |
| `nonRetryableErrorCodes` | none                                     | API error codes that never retry.                                                  |
| `maxRetryAfterSeconds`   | `30`                                     | The longest `Retry-After` a retry waits for.                                       |
| `maxResponseBytes`       | 10 MiB                                   | The largest body the buffered modes read.                                          |

Network errors, attempt timeouts, and retryable statuses retry. A `429` retries only with `Retry-After`, and every retry waits the longer of its delay and `Retry-After`. When `Retry-After` asks for more than `maxRetryAfterSeconds`, or more than the time left, the call stops and the error carries `retryAfterMs`. A stream body never retries.

Pass `signal` to cancel: its abort reports `ABORTED`, or `TIMEOUT` when its reason is a `TimeoutError`, such as `AbortSignal.timeout(5000)`'s.

### Errors

The error is a `FetchException`, an `Exception` from `jg-exception`:

| Field                                     | Holds                                                                                        |
| ----------------------------------------- | -------------------------------------------------------------------------------------------- |
| `kind`                                    | What failed, one of `ERRORS`                                                                 |
| `code`, `message`                         | The envelope error's code and message for an `API_ERROR`, otherwise the kind and its message |
| `status`, `headers`, `body`               | The response's status, headers, and body, parsed as JSON when it is JSON                     |
| `data`                                    | The envelope error's data for an `API_ERROR`, `null` when it has none; otherwise undefined   |
| `retryAfterMs`                            | The wait `Retry-After` asked for                                                             |
| `method`, `url`, `attempts`, `durationMs` | The request, without its query string or fragment                                            |
| `cause`                                   | The original error                                                                           |

Messages never include the URL or the body, and `url` and `meta.url` drop the query string and fragment, where tokens such as `?key=` sit.

### Reading headers across origins

A browser lets the page read only a few response headers from another origin. To read `Retry-After`, a `cf-ray`, or any custom header, the API must list them in `Access-Control-Expose-Headers`. Otherwise `headers.get()` returns `null`, and a `429` never retries, because its `Retry-After` can't be read.

## <a id="api-reference"></a>jg-fetch API reference

Every export of `jg-fetch@0.3.0`, 26 in all, with its signature, its errors, and an example. It's generated from the package's TSDoc.

- Functions: [`parseRetryAfterMs`](#api-parseRetryAfterMs), [`request`](#api-request)
- Classes: [`FetchException`](#api-FetchException)
- Constants: [`DEFAULT_MAX_RESPONSE_BYTES`](#api-DEFAULT_MAX_RESPONSE_BYTES), [`DEFAULT_MAX_RETRY_AFTER_SECONDS`](#api-DEFAULT_MAX_RETRY_AFTER_SECONDS), [`DEFAULT_RETRY_DELAYS_SECONDS`](#api-DEFAULT_RETRY_DELAYS_SECONDS), [`DEFAULT_RETRY_METHODS`](#api-DEFAULT_RETRY_METHODS), [`DEFAULT_RETRY_STATUS_CODES`](#api-DEFAULT_RETRY_STATUS_CODES), [`DEFAULT_TIMEOUT_SECONDS`](#api-DEFAULT_TIMEOUT_SECONDS), [`ERRORS`](#api-ERRORS), [`MAX_TIMEOUT_SECONDS`](#api-MAX_TIMEOUT_SECONDS), [`NON_RETRYABLE_STATUS_CODES`](#api-NON_RETRYABLE_STATUS_CODES)
- Types: [`IAttemptContext`](#api-IAttemptContext), [`ICredentialFailure`](#api-ICredentialFailure), [`IFetchErrorKind`](#api-IFetchErrorKind), [`IFetchExceptionInit`](#api-IFetchExceptionInit), [`IHeadersInit`](#api-IHeadersInit), [`IHttpMethod`](#api-IHttpMethod), [`IParseAs`](#api-IParseAs), [`IQueryParams`](#api-IQueryParams), [`IQueryScalar`](#api-IQueryScalar), [`IRequestOptions`](#api-IRequestOptions), [`IRequestResult`](#api-IRequestResult), [`IResponseMeta`](#api-IResponseMeta), [`IRetryContext`](#api-IRetryContext), [`ISchema`](#api-ISchema)

### <a id="api-functions"></a>Functions

#### <a id="api-parseRetryAfterMs"></a>`parseRetryAfterMs`

*Function*

Parses a Retry-After header into the milliseconds to wait. It accepts a delay in seconds, such as `120`, or an HTTP date, such as `Wed, 21 Oct 2015 07:28:00 GMT`, whose wait is counted from nowMs. A date in the past waits 0, and a wait longer than MAX_TIMEOUT_SECONDS is clamped to it. For a header with several values, such as a WebSocket handshake's, the first one counts.

```ts
parseRetryAfterMs(
  value: string | readonly string[] | null | undefined,
  nowMs?: number,
): number | null
```

**Parameters**

- `value` `string | readonly string[] | null | undefined`: The header's value, or its values.
- `nowMs?` `number`: The current time, in milliseconds since the epoch. Defaults to Date.now(). Defaults to `...`.

**Returns** `number | null`: The milliseconds to wait, or null when the value is missing or invalid.

**Example**

```ts
import { parseRetryAfterMs } from 'jg-fetch';

parseRetryAfterMs('120'); // 120000
parseRetryAfterMs('Wed, 21 Oct 2015 07:28:30 GMT', Date.parse('Wed, 21 Oct 2015 07:28:00 GMT')); // 30000
parseRetryAfterMs('soon'); // null
```

#### <a id="api-request"></a>`request`

*Function*

Sends an HTTP request and resolves to its data or its error, never throwing. It retries network errors, attempt timeouts, and retryable statuses for the methods that may retry, waiting retryDelaysSeconds between attempts, or longer when the server sends Retry-After, all within timeoutSeconds. Each attempt sends a fresh Request. With parseAs 'envelope', it reads a jg-api-response envelope and resolves to its data, or to API_ERROR with the API's own code. With refreshCredentials, an attempt that fails with a response, such as a 401, can refresh the credentials and repeat at once, up to maxCredentialRefreshes times, without using up a retry.

```ts
request(
  url: string | URL,
  options: IRequestOptions<string> & { parseAs: "text" },
): Promise<IRequestResult<string>>
```

**Parameters**

- `url` `string | URL`: The URL, absolute or relative to baseUrl.
- `options` `IRequestOptions<string> & { parseAs: "text" }`: The method, query, headers, body, how to read the response, and the timeouts and retries.

**Returns** `Promise<IRequestResult<string>>`: A promise that resolves to `{ data, error: null, meta }` or `{ data: null, error, meta }`, where error is a FetchException.

```ts
request(
  url: string | URL,
  options: IRequestOptions<Uint8Array<ArrayBufferLike>> & { parseAs: "bytes" },
): Promise<IRequestResult<Uint8Array<ArrayBufferLike>>>
```

**Parameters**

- `url` `string | URL`: The URL, absolute or relative to baseUrl.
- `options` `IRequestOptions<Uint8Array<ArrayBufferLike>> & { parseAs: "bytes" }`: The method, query, headers, body, how to read the response, and the timeouts and retries.

**Returns** `Promise<IRequestResult<Uint8Array<ArrayBufferLike>>>`: A promise that resolves to `{ data, error: null, meta }` or `{ data: null, error, meta }`, where error is a FetchException.

```ts
request(
  url: string | URL,
  options: IRequestOptions<Blob> & { parseAs: "blob" },
): Promise<IRequestResult<Blob>>
```

**Parameters**

- `url` `string | URL`: The URL, absolute or relative to baseUrl.
- `options` `IRequestOptions<Blob> & { parseAs: "blob" }`: The method, query, headers, body, how to read the response, and the timeouts and retries.

**Returns** `Promise<IRequestResult<Blob>>`: A promise that resolves to `{ data, error: null, meta }` or `{ data: null, error, meta }`, where error is a FetchException.

```ts
request(
  url: string | URL,
  options: IRequestOptions<Response> & { parseAs: "response" },
): Promise<IRequestResult<Response>>
```

**Parameters**

- `url` `string | URL`: The URL, absolute or relative to baseUrl.
- `options` `IRequestOptions<Response> & { parseAs: "response" }`: The method, query, headers, body, how to read the response, and the timeouts and retries.

**Returns** `Promise<IRequestResult<Response>>`: A promise that resolves to `{ data, error: null, meta }` or `{ data: null, error, meta }`, where error is a FetchException.

```ts
request(
  url: string | URL,
  options: IRequestOptions<undefined> & { parseAs: "none" },
): Promise<IRequestResult<undefined>>
```

**Parameters**

- `url` `string | URL`: The URL, absolute or relative to baseUrl.
- `options` `IRequestOptions<undefined> & { parseAs: "none" }`: The method, query, headers, body, how to read the response, and the timeouts and retries.

**Returns** `Promise<IRequestResult<undefined>>`: A promise that resolves to `{ data, error: null, meta }` or `{ data: null, error, meta }`, where error is a FetchException.

```ts
request<T = unknown>(
  url: string | URL,
  options?: IRequestOptions<T>,
): Promise<IRequestResult<T>>
```

**Parameters**

- `url` `string | URL`: The URL, absolute or relative to baseUrl.
- `options?` `IRequestOptions<T>`: The method, query, headers, body, how to read the response, and the timeouts and retries.

**Returns** `Promise<IRequestResult<T>>`: A promise that resolves to `{ data, error: null, meta }` or `{ data: null, error, meta }`, where error is a FetchException.

**Example 1**

```ts
import { request } from 'jg-fetch';

const { data, error } = await request<IUser>('/users/1', {
  baseUrl: 'https://api.example.com',
  parseAs: 'envelope',
  headers: async () => ({ authorization: `Bearer ${await getToken()}` }),
});
if (error) {
  error.kind; // such as 'API_ERROR' or 'TIMEOUT'
}
```

**Example 2**

```ts
import { request } from 'jg-fetch';

const { data: html } = await request('https://example.com', { parseAs: 'text' }); // string | null
```

### <a id="api-classes"></a>Classes

#### <a id="api-FetchException"></a>`FetchException`

*Class*

The error request returns when a call fails, never throws. kind says what failed, one of the ERRORS codes. For an API error, code, message, and data are the envelope error's; otherwise code is the kind. Its message never includes the URL or the body.

**Members**

- `code: string` (property)
- `data: IJsonValue | undefined` (property)
- `severity: "expected" | "critical"` (property)
- `shouldLog: boolean` (property)
- `toJSON(): IExceptionJson` (method): Converts the exception to a plain object, which `JSON.stringify` calls.
- `toString(): string` (method): Describes the exception in one line, with its code.
- `kind: "INVALID_URL" | "INVALID_OPTIONS" | "INVALID_HEADERS" | "INVALID_BODY" | "NETWORK_ERROR" | "TIMEOUT" | "ABORTED" | "HTTP_ERROR" | "API_ERROR" | "PARSE_ERROR" | "ENVELOPE_INVALID" | "RESPONSE_INVALID" | "RESPONSE_TOO_LARGE" | "CREDENTIALS_REFRESH_FAILED"` (property): what failed, one of the ERRORS codes
- `status: number | null` (property): the HTTP status, or null when no response arrived
- `headers: Headers | null` (property): the response's headers, or null when no response arrived
- `body: unknown` (property): the response body: parsed JSON when it is JSON, otherwise its text
- `retryAfterMs: number | null` (property): the wait the server asked for with Retry-After, in milliseconds, or null without one
- `method: IHttpMethod` (property): the request's method
- `url: string` (property): the request's URL, without its query string or fragment
- `attempts: number` (property): the number of attempts made
- `durationMs: number` (property): the milliseconds the whole call took
- `new FetchException(kind: "INVALID_URL" | "INVALID_OPTIONS" | "INVALID_HEADERS" | "INVALID_BODY" | "NETWORK_ERROR" | "TIMEOUT" | "ABORTED" | "HTTP_ERROR" | "API_ERROR" | "PARSE_ERROR" | "ENVELOPE_INVALID" | "RESPONSE_INVALID" | "RESPONSE_TOO_LARGE" | "CREDENTIALS_REFRESH_FAILED", init: IFetchExceptionInit)` (constructor)

**Example**

```ts
import { request } from 'jg-fetch';

const { data, error } = await request<IUser>('/users/1', { baseUrl, parseAs: 'envelope' });
if (error) {
  error.kind; // 'API_ERROR'
  error.code; // 'USER_NOT_FOUND', the envelope error's code
  error.data; // { id: 1 }, the envelope error's data
  error.status; // 404
}
```

### <a id="api-constants"></a>Constants

#### <a id="api-DEFAULT_MAX_RESPONSE_BYTES"></a>`DEFAULT_MAX_RESPONSE_BYTES`

*Constant*

the largest response body the buffered parseAs modes read, in bytes: 10 MiB

```ts
const DEFAULT_MAX_RESPONSE_BYTES: 10485760
```

#### <a id="api-DEFAULT_MAX_RETRY_AFTER_SECONDS"></a>`DEFAULT_MAX_RETRY_AFTER_SECONDS`

*Constant*

the longest Retry-After wait a retry accepts, in seconds, unless maxRetryAfterSeconds says otherwise

```ts
const DEFAULT_MAX_RETRY_AFTER_SECONDS: 30
```

#### <a id="api-DEFAULT_RETRY_DELAYS_SECONDS"></a>`DEFAULT_RETRY_DELAYS_SECONDS`

*Constant*

the waits before each retry, in seconds: two retries, after 3 and then 5 seconds

```ts
const DEFAULT_RETRY_DELAYS_SECONDS: readonly number[]
```

#### <a id="api-DEFAULT_RETRY_METHODS"></a>`DEFAULT_RETRY_METHODS`

*Constant*

the methods that retry by default, which are safe to repeat

```ts
const DEFAULT_RETRY_METHODS: readonly IHttpMethod[]
```

#### <a id="api-DEFAULT_RETRY_STATUS_CODES"></a>`DEFAULT_RETRY_STATUS_CODES`

*Constant*

the statuses that retry by default, because a later attempt can succeed

```ts
const DEFAULT_RETRY_STATUS_CODES: readonly number[]
```

#### <a id="api-DEFAULT_TIMEOUT_SECONDS"></a>`DEFAULT_TIMEOUT_SECONDS`

*Constant*

the seconds a whole call may take, retries and waits included, unless timeoutSeconds says otherwise

```ts
const DEFAULT_TIMEOUT_SECONDS: 15
```

#### <a id="api-ERRORS"></a>`ERRORS`

*Constant*

the codes for every error request reports, which FetchException's kind holds

```ts
const ERRORS: { INVALID_URL: "INVALID_URL"; INVALID_OPTIONS: "INVALID_OPTIONS"; INVALID_HEADERS: "INVALID_HEADERS"; INVALID_BODY: "INVALID_BODY"; NETWORK_ERROR: "NETWORK_ERROR"; TIMEOUT: "TIMEOUT"; ABORTED: "ABORTED"; HTTP_ERROR: "HTTP_ERROR"; API_ERROR: "API_ERROR"; PARSE_ERROR: "PARSE_ERROR"; ENVELOPE_INVALID: "ENVELOPE_INVALID"; RESPONSE_INVALID: "RESPONSE_INVALID"; RESPONSE_TOO_LARGE: "RESPONSE_TOO_LARGE"; CREDENTIALS_REFRESH_FAILED: "CREDENTIALS_REFRESH_FAILED" }
```

#### <a id="api-MAX_TIMEOUT_SECONDS"></a>`MAX_TIMEOUT_SECONDS`

*Constant*

the longest timeout a timer can hold, in seconds, which is 2^31 - 1 milliseconds

```ts
const MAX_TIMEOUT_SECONDS: 2147483
```

#### <a id="api-NON_RETRYABLE_STATUS_CODES"></a>`NON_RETRYABLE_STATUS_CODES`

*Constant*

the statuses that never retry, even when retryStatusCodes lists them

```ts
const NON_RETRYABLE_STATUS_CODES: readonly number[]
```

### <a id="api-types"></a>Types

#### <a id="api-IAttemptContext"></a>`IAttemptContext`

*Type*

what a headers function receives on each attempt

```ts
type IAttemptContext = {
    attempt: number;
    signal: AbortSignal;
};
```

**Members**

- `attempt: number` (property): the attempt's number, starting at 1
- `signal: AbortSignal` (property): aborts when the attempt is cancelled, for work such as refreshing a token

#### <a id="api-ICredentialFailure"></a>`ICredentialFailure`

*Type*

what refreshCredentials receives when an attempt fails with a response

```ts
type ICredentialFailure = {
    status: number;
    code: string;
    refreshes: number;
    signal: AbortSignal;
};
```

**Members**

- `status: number` (property): the response's HTTP status, such as 401
- `code: string` (property): the error's code: the envelope error's own code, or HTTP_ERROR
- `refreshes: number` (property): the refreshes this call already made
- `signal: AbortSignal` (property): aborts when the call is cancelled, by its timeout or your signal, for the refresh's own work

#### <a id="api-IFetchErrorKind"></a>`IFetchErrorKind`

*Type*

the kind of a failed request, one of the ERRORS codes

```ts
type IFetchErrorKind = keyof typeof ERRORS;
```

#### <a id="api-IFetchExceptionInit"></a>`IFetchExceptionInit`

*Type*

what a FetchException carries besides its kind

```ts
type IFetchExceptionInit = {
    message?: string;
    code?: string;
    data?: IJsonValue;
    status?: number | null;
    headers?: Headers | null;
    body?: unknown;
    retryAfterMs?: number | null;
    method: IHttpMethod;
    url: string;
    attempts: number;
    durationMs: number;
    cause?: unknown;
};
```

**Members**

- `message: string` (property): the message. Defaults to the kind's message
- `code: string` (property): the code. Defaults to the kind; an API error's own code takes its place
- `data: IJsonValue` (property): the envelope error's data, for an API error
- `status: number | null` (property): the HTTP status, or null when no response arrived
- `headers: Headers | null` (property): the response's headers, or null when no response arrived
- `body: unknown` (property): the response body: parsed JSON when it is JSON, otherwise its text
- `retryAfterMs: number | null` (property): the wait the server asked for with Retry-After, in milliseconds
- `method: IHttpMethod` (property): the request's method
- `url: string` (property): the request's URL, without its query string or fragment
- `attempts: number` (property): the number of attempts made
- `durationMs: number` (property): the milliseconds the whole call took
- `cause: unknown` (property): the original error, such as a TypeError from fetch or a schema's error

#### <a id="api-IHeadersInit"></a>`IHeadersInit`

*Type*

the headers the Headers constructor accepts: Headers, [name, value] pairs, or a record

```ts
type IHeadersInit = ConstructorParameters<typeof Headers>[0];
```

#### <a id="api-IHttpMethod"></a>`IHttpMethod`

*Type*

the HTTP methods request sends

```ts
type IHttpMethod = 'GET' | 'HEAD' | 'OPTIONS' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
```

#### <a id="api-IParseAs"></a>`IParseAs`

*Type*

how request reads a successful response: - envelope: a jg-api-response envelope, resolving to its data - json: parsed JSON - text: a string - bytes: a Uint8Array - blob: a Blob - response: the Response itself, unread - none: nothing, the body is discarded

```ts
type IParseAs = 'envelope' | 'json' | 'text' | 'bytes' | 'blob' | 'response' | 'none';
```

#### <a id="api-IQueryParams"></a>`IQueryParams`

*Type*

the query parameters appended to the URL. An array repeats the key once per item

```ts
type IQueryParams = Record<string, IQueryScalar | readonly IQueryScalar[]>;
```

#### <a id="api-IQueryScalar"></a>`IQueryScalar`

*Type*

a query parameter's value. null, undefined, and empty strings are left out

```ts
type IQueryScalar = string | number | boolean | bigint | null | undefined;
```

#### <a id="api-IRequestOptions"></a>`IRequestOptions`

*Type*

the options request accepts

```ts
type IRequestOptions<T> = {
    method?: IHttpMethod;
    baseUrl?: string | URL;
    query?: IQueryParams;
    headers?: IHeadersInit | ((context: IAttemptContext) => IHeadersInit | Promise<IHeadersInit>);
    body?: RequestInit['body'] | Record<string, unknown> | readonly unknown[];
    parseAs?: IParseAs;
    schema?: ISchema<T>;
    signal?: AbortSignal;
    timeoutSeconds?: number;
    attemptTimeoutSeconds?: number;
    retryDelaysSeconds?: readonly number[];
    retryMethods?: readonly IHttpMethod[];
    retryStatusCodes?: readonly number[];
    nonRetryableErrorCodes?: readonly string[];
    maxRetryAfterSeconds?: number;
    maxResponseBytes?: number;
    init?: Omit<RequestInit, 'method' | 'headers' | 'body' | 'signal'>;
    onRetry?: (context: IRetryContext) => void;
    refreshCredentials?: (failure: ICredentialFailure) => boolean | Promise<boolean>;
    maxCredentialRefreshes?: number;
};
```

**Members**

- `method: IHttpMethod` (property): the HTTP method. Defaults to 'GET'
- `baseUrl: string | URL` (property): the URL a relative url resolves against. A URL that resolves to another origin fails with INVALID_URL
- `query: IQueryParams` (property): the query parameters appended to the URL
- `headers: IHeadersInit | ((context: IAttemptContext) => IHeadersInit | Promise<IHeadersInit>)` (property): the headers, or a function that builds them for each attempt, such as to add a fresh token
- `body: RequestInit["body"] | Record<string, unknown> | readonly unknown[]` (property): the body. Plain objects and arrays are sent as JSON, with Content-Type: application/json unless the headers set one. A string, Blob, ArrayBuffer or view of one, FormData, URLSearchParams, or ReadableStream is sent as it is. Any other value, such as a Map, a Date, or a number, is an INVALID_BODY error, sent nowhere
- `parseAs: IParseAs` (property): how a successful response is read. Defaults to 'json'
- `schema: ISchema<T>` (property): validates the parsed data, or the envelope's data, such as a Zod schema
- `signal: AbortSignal` (property): aborts the call. An abort whose reason is a TimeoutError, such as AbortSignal.timeout's, reports TIMEOUT, and any other ABORTED
- `timeoutSeconds: number` (property): the seconds the whole call may take, retries and waits included. Defaults to DEFAULT_TIMEOUT_SECONDS
- `attemptTimeoutSeconds: number` (property): the seconds each attempt may take. An attempt that runs out retries. No limit by default
- `retryDelaysSeconds: readonly number[]` (property): the waits before each retry, in seconds; one retry per item. Defaults to DEFAULT_RETRY_DELAYS_SECONDS, and [] turns retries off
- `retryMethods: readonly IHttpMethod[]` (property): the methods that may retry. Defaults to DEFAULT_RETRY_METHODS
- `retryStatusCodes: readonly number[]` (property): the statuses that may retry. Defaults to DEFAULT_RETRY_STATUS_CODES, and NON_RETRYABLE_STATUS_CODES never retry
- `nonRetryableErrorCodes: readonly string[]` (property): the API error codes, from an envelope's error, that never retry
- `maxRetryAfterSeconds: number` (property): the longest Retry-After wait a retry accepts, in seconds. Defaults to DEFAULT_MAX_RETRY_AFTER_SECONDS
- `maxResponseBytes: number` (property): the largest body the buffered parseAs modes read, counted as it streams. Defaults to DEFAULT_MAX_RESPONSE_BYTES
- `init: Omit<RequestInit, "method" | "headers" | "body" | "signal">` (property): the other fetch options, such as credentials, cache, or redirect
- `onRetry: (context: IRetryContext) => void` (property): called before each retry. Whatever it throws is ignored, so request still never throws
- `refreshCredentials: (failure: ICredentialFailure) => boolean | Promise<boolean>` (property): called when an attempt fails with a response, such as a 401, to refresh the credentials the headers function reads. Returning true repeats the attempt at once, for any method; false leaves the failure to the retry rules. Return true only for a credential failure, because the server didn't process the request then. A function that throws ends the call with CREDENTIALS_REFRESH_FAILED. Never called for a streamed body, which can be sent once
- `maxCredentialRefreshes: number` (property): the most refreshes a call makes, such as 2 for two kinds of credential. Defaults to 1

#### <a id="api-IRequestResult"></a>`IRequestResult`

*Type*

what request resolves to: the data and no error, or no data and the error

```ts
type IRequestResult<T> = {
    data: T;
    error: null;
    meta: IResponseMeta;
} | {
    data: null;
    error: FetchException;
    meta: IResponseMeta;
};
```

#### <a id="api-IResponseMeta"></a>`IResponseMeta`

*Type*

what request knows about the last response, successful or not

```ts
type IResponseMeta = {
    status: number | null;
    headers: Headers | null;
    url: string;
    attempts: number;
    durationMs: number;
};
```

**Members**

- `status: number | null` (property): the HTTP status, or null when no response arrived
- `headers: Headers | null` (property): the response's headers, or null when no response arrived
- `url: string` (property): the URL, without its query string or fragment, which can hold tokens
- `attempts: number` (property): the number of attempts made
- `durationMs: number` (property): the milliseconds the whole call took, retries and waits included

#### <a id="api-IRetryContext"></a>`IRetryContext`

*Type*

what onRetry receives before each retry

```ts
type IRetryContext = {
    attempt: number;
    error: FetchException;
    delayMs: number;
};
```

**Members**

- `attempt: number` (property): the attempt that failed, starting at 1
- `error: FetchException` (property): why it failed
- `delayMs: number` (property): the milliseconds request waits before the next attempt

#### <a id="api-ISchema"></a>`ISchema`

*Type*

validates parsed data and returns it typed, which a Zod schema does

```ts
type ISchema<T> = {
    parse(value: unknown): T;
};
```

**Members**

- `parse(value: unknown): T` (method)
