Skip to content

@absoluteui/services/http

fetch client with interceptors, lifecycle hooks, cancellation (all, by group, latest-wins), safe retries, deduplication, ETag caching, categorized errors with default status messages, pagination, and upload progress.

import { ... } from '@absoluteui/services/http';

A fetch client for application APIs: timeouts, safe retries, cancellation, typed JSON guards, categorized errors with user-facing messages, interceptors, hooks, pagination, and uploads with progress. The HTTP client guide covers interceptors, hooks, cancellation, and error handling in depth; the recipes below are the everyday calls. The examples use createMockFetch from @absoluteui/services/http-mock; in an application, omit fetch.

How do I call a JSON API with typed results?

Section titled “How do I call a JSON API with typed results?”

Create one client per API. get, post, put, patch, and delete resolve with the parsed body; pass validate to get a typed value and a bad-response error when the server sends something else.

import { createHttpClient } from '@absoluteui/services/http';
import { createMockFetch } from '@absoluteui/services/http-mock';
interface Employee { id: number; name: string; department: string }
const isEmployee = (d: unknown): d is Employee =>
typeof d === 'object' && d !== null && 'id' in d && typeof d.id === 'number' && 'name' in d && 'department' in d;
const fetch = createMockFetch([{ method: 'GET', url: '/api/employees/42', body: { id: 42, name: 'Ada Lovelace', department: 'Finance' } }]);
const api = createHttpClient({ baseUrl: 'http://localhost/api', timeoutMs: 15_000, fetch });
const ada = await api.get('/employees/42', { validate: isEmployee });
ada.department; // => 'Finance'
fetch.calls[0]?.request.url.href; // => 'http://localhost/api/employees/42'

How do I show validation errors from the server next to the fields?

Section titled “How do I show validation errors from the server next to the fields?”

Every failure rejects with an HttpError. Its category says what happened, message is ready for the user, and fields holds per-field messages from an RFC 9457 Problem Details body.

import { HttpError } from '@absoluteui/helpers/error';
import { createHttpClient } from '@absoluteui/services/http';
import { createMockFetch } from '@absoluteui/services/http-mock';
const fetch = createMockFetch([
{ method: 'POST', url: '/api/leave', status: 422, body: { title: 'Invalid request', status: 422, errors: { to: 'The end date must be on or after the start date.' } } },
]);
const api = createHttpClient({ baseUrl: 'http://localhost/api', fetch });
const error = await api.post('/leave', { from: '2026-10-09', to: '2026-10-05' }).catch((e: unknown) => e);
error instanceof HttpError; // => true
const { category, status, fields } = error as HttpError;
[category, status]; // => ['validation', 422]
fields; // => { to: 'The end date must be on or after the start date.' }

paginate is an async generator over Link headers, cursors, or offsets. It stops at a short page, at the reported total, or after maxPages (default 100).

import { createHttpClient, paginate } from '@absoluteui/services/http';
import { createMockFetch } from '@absoluteui/services/http-mock';
const fetch = createMockFetch([
{ url: '/api/employees?offset=0&limit=2', body: { items: [{ id: 1 }, { id: 2 }] } },
{ url: '/api/employees?offset=2&limit=2', body: { items: [{ id: 3 }] } },
]);
const api = createHttpClient({ baseUrl: 'http://localhost/api', fetch });
const ids: number[] = [];
const toItems = (d: unknown): { id: number }[] => (typeof d === 'object' && d !== null && 'items' in d && Array.isArray(d.items) ? d.items : []);
for await (const page of paginate(api, '/employees', { strategy: 'offset', pageSize: 2, items: toItems })) ids.push(...page.items.map((e) => e.id));
ids; // => [1, 2, 3]

How do I upload a file with a progress bar?

Section titled “How do I upload a file with a progress bar?”

fetch cannot report upload progress, so uploadWithProgress uses XMLHttpRequest. For large files that must survive a dropped connection, use chunked uploads from @absoluteui/services/upload.

import { uploadWithProgress } from '@absoluteui/services/http';
declare const file: File; // from a file input
const bar = document.createElement('progress');
bar.max = 100;
const response = await uploadWithProgress('/api/employees/42/documents', file, {
onProgress: (p) => { bar.value = p.percent ?? 0; },
});
  • Retries apply only to idempotent requests (GET, HEAD, OPTIONS, PUT, DELETE) and to POST or PATCH with an idempotencyKey; a plain POST is never sent twice.
  • A response that fails validate is an error in the bad-response category, not a wrongly typed value.
  • Server error text is not shown to users unless you set trustServerMessages; the default messages never leak internals.

TransferProgress

Download or upload progress. total and percent are null when the size is unknown.

interface TransferProgress {
readonly loaded: number;
readonly total: number | null;
readonly percent: number | null;
}

createHttpClient

Creates a fetch client with timeouts, idempotent-only retries (honouring Retry-After), in-flight GET de-duplication, an optional ETag cache, typed JSON guards, RFC 9457 problem details on errors, auth refresh and CSRF headers, runtime interceptors, lifecycle hooks, cancellation (all, by group, or latest-wins), and child clients.

Every transport or HTTP failure rejects with an HttpError whose category says why: non-2xx -> SVC_HTTP_STATUS (message from the catalog, fields from problem details); network failure -> status 0 SVC_HTTP_NETWORK (network or offline); deadline -> SVC_HTTP_TIMEOUT (timeout); cancellation -> SVC_HTTP_ABORTED (cancelled, never retried or reported); unparsable or invalid body -> SVC_HTTP_INVALID_RESPONSE (bad-response); maxResponseBytes exceeded -> SVC_HTTP_RESPONSE_TOO_LARGE (too-large). Invalid arguments throw ValidationError. Redirects follow fetch defaults: browsers drop Authorization on cross-origin redirects and cookies follow credentials (default 'same-origin').

function createHttpClient(options?: HttpClientOptions): HttpClient;

Example:

interface User { id: number; name: string }
const isUser = (d: unknown): d is User => typeof d === 'object' && d !== null && 'id' in d && 'name' in d;
const api = createHttpClient({
baseUrl: '/api',
auth: { getToken: async () => sessionStorage.getItem('token'), refresh: async () => { await fetch('/auth/refresh', { method: 'POST' }); } },
hooks: { onError: (error) => console.warn(error.message) },
});
const user = await api.get('/users/42', { validate: isUser });
await api.post('/orders', { sku: 'LAPTOP-14', qty: 1 }, { idempotencyKey: true });

Example:

// Interceptors transform; child clients inherit and add.
import { createMockFetch } from '@absoluteui/services/http-mock';
const fetch = createMockFetch([{ url: /./, respond: (r) => ({ body: { tenant: r.headers.get('x-tenant'), path: r.url.pathname } }) }]);
const api = createHttpClient({ fetch, baseUrl: 'http://localhost/api' });
const remove = api.interceptors.request.use((req) => { req.headers.set('x-tenant', 'acme'); return undefined; });
const hr = api.extend({ baseUrl: 'http://localhost/hr' });
await hr.get('/employees'); // => { tenant: 'acme', path: '/hr/employees' }
remove();
await api.get('/employees'); // => { tenant: null, path: '/api/employees' }

Example:

// Cancellation: latest wins, groups, everything.
import { createMockFetch } from '@absoluteui/services/http-mock';
import { isCancelled } from '@absoluteui/services/http';
const fetch = createMockFetch([{ url: /./, delayMs: 20, respond: (r) => ({ body: [r.url.searchParams.get('q')] }) }]);
const api = createHttpClient({ fetch, baseUrl: 'http://localhost' });
const first = api.get('/search', { query: { q: 'ad' }, cancelKey: 'people' });
const second = api.get('/search', { query: { q: 'ada' }, cancelKey: 'people' });
isCancelled(await first.catch((e: unknown) => e)); // => true
await second; // => ['ada']
const report = api.get('/report', { group: 'reports' });
api.cancelGroup('reports'); // => 1
isCancelled(await report.catch((e: unknown) => e)); // => true

Example:

// Errors and hooks: categories, default messages, one report for parallel failures.
import { createMockFetch } from '@absoluteui/services/http-mock';
import { HttpError } from '@absoluteui/helpers/error';
const fetch = createMockFetch([{ url: /./, status: 401 }]);
const reported: string[] = [];
const outcomes: string[] = [];
const api = createHttpClient({ fetch, retry: false, reportError: (e) => { reported.push(e.message); }, hooks: { onComplete: (r) => { outcomes.push(r.outcome); } } });
const [e] = await Promise.all([api.get('http://localhost/a').catch((x: unknown) => x), api.get('http://localhost/b').catch((x: unknown) => x)]);
e instanceof HttpError && e.category; // => 'unauthorized'
reported; // => ['Your session has ended. Sign in again to continue.']
outcomes; // => ['error', 'error']

EtagEntry

Cached representation for conditional GETs.

interface EtagEntry {
readonly etag: string;
readonly body: unknown;
}

EtagStore

Storage for the ETag cache (sync or async). Keys never contain credentials.

interface EtagStore {
get(key: string): Awaitable<EtagEntry | undefined>;
set(key: string, entry: EtagEntry): Awaitable<void>;
delete(key: string): Awaitable<void>;
}

HttpAuth

Bearer-token hook. Concurrent 401s share one refresh() and each request is retried once.

interface HttpAuth {
getToken(): Promise<string | null>;
refresh?(): Promise<void>;
readonly scheme?: string;
}

HttpCallInfo

One call (get, post, send, …) as hooks and pending see it.

interface HttpCallInfo {
readonly id: number;
readonly method: string;
readonly url: string;
readonly group: string | undefined;
readonly cancelKey: string | undefined;
readonly startedAt: number;
}

HttpCallResult

Passed to onComplete, exactly once per call.

interface HttpCallResult {
readonly call: HttpCallInfo;
readonly outcome: HttpOutcome;
readonly durationMs: number;
readonly response: HttpResponse<unknown> | undefined;
readonly error: Error | undefined;
}

HttpClient

Client returned by createHttpClient.

interface HttpClient {
request<T = unknown, R extends ResponseType = 'json'>(method: string, url: string, options?: RequestOptions<T, R>): Promise<ResponseTypeMap<T>[R]>;
send<T = unknown, R extends ResponseType = 'json'>(method: string, url: string, options?: RequestOptions<T, R>): Promise<HttpResponse<ResponseTypeMap<T>[R]>>;
get<T = unknown, R extends ResponseType = 'json'>(url: string, options?: BodylessOptions<T, R>): Promise<ResponseTypeMap<T>[R]>;
head(url: string, options?: BodylessOptions<unknown, 'response'>): Promise<Response>;
delete<T = unknown, R extends ResponseType = 'json'>(url: string, options?: BodylessOptions<T, R>): Promise<ResponseTypeMap<T>[R]>;
post<T = unknown, R extends ResponseType = 'json'>(url: string, body?: unknown, options?: BodylessOptions<T, R>): Promise<ResponseTypeMap<T>[R]>;
put<T = unknown, R extends ResponseType = 'json'>(url: string, body?: unknown, options?: BodylessOptions<T, R>): Promise<ResponseTypeMap<T>[R]>;
patch<T = unknown, R extends ResponseType = 'json'>(url: string, body?: unknown, options?: BodylessOptions<T, R>): Promise<ResponseTypeMap<T>[R]>;
readonly interceptors: HttpInterceptors;
extend(options?: HttpClientOptions): HttpClient;
readonly pending: readonly HttpCallInfo[];
cancelAll(reason?: string): number;
cancelGroup(group: string, reason?: string): number;
onPendingChange(listener: (count: number) => void): () => void;
}

HttpClientOptions

interface HttpClientOptions {
readonly baseUrl?: string;
readonly headers?: HeadersInit | (() => Awaitable<HeadersInit>);
readonly timeoutMs?: number;
readonly retry?: Partial<RetryPolicy> | false;
readonly credentials?: RequestCredentials;
readonly fetch?: FetchLike;
readonly onRequest?: RequestInterceptor | readonly RequestInterceptor[];
readonly onResponse?: ResponseInterceptor | readonly ResponseInterceptor[];
readonly onError?: ErrorInterceptor | readonly ErrorInterceptor[];
readonly hooks?: HttpHooks;
readonly dedupeGets?: boolean;
readonly etagCache?: boolean | EtagStore;
readonly maxResponseBytes?: number;
readonly json?: SafeJsonParseOptions;
readonly auth?: HttpAuth;
readonly xsrf?: XsrfOptions | boolean;
readonly messages?: StatusMessageCatalog;
readonly trustServerMessages?: TrustServerMessages;
readonly reportError?: (error: Error) => void;
readonly reportDedupeMs?: number;
readonly mapError?: (error: Error) => Error;
readonly logger?: HttpLogger;
}

HttpHooks

Lifecycle observers. Hooks never change the request or response (use interceptors for that), are not awaited, and a hook that throws or rejects is reported to logger.warn and ignored. Client hooks run before child-client hooks, which run before per-request hooks.

interface HttpHooks {
beforeRequest?(request: HttpRequestInfo): void | Promise<void>;
afterRequest?(info: {
readonly request: HttpRequestInfo;
readonly response: Response | undefined;
readonly error: Error | undefined;
readonly durationMs: number;
}): void | Promise<void>;
beforeResponse?(info: {
readonly request: HttpRequestInfo | undefined;
readonly response: Response;
}): void | Promise<void>;
afterResponse?(info: {
readonly call: HttpCallInfo;
readonly response: HttpResponse<unknown>;
}): void | Promise<void>;
onRetry?(info: {
readonly call: HttpCallInfo;
readonly error: Error;
readonly attempt: number;
readonly delayMs: number;
}): void | Promise<void>;
onSuccess?(response: HttpResponse<unknown>, call: HttpCallInfo): void | Promise<void>;
onError?(error: Error, call: HttpCallInfo): void | Promise<void>;
onCancel?(error: HttpError, call: HttpCallInfo): void | Promise<void>;
onComplete?(result: HttpCallResult): void | Promise<void>;
}

HttpInterceptors

Runtime interceptor registration.

interface HttpInterceptors {
readonly request: InterceptorRegistry<RequestInterceptor>;
readonly response: InterceptorRegistry<ResponseInterceptor>;
readonly error: InterceptorRegistry<ErrorInterceptor>;
}

HttpLogger

Diagnostic sink; receives redacted headers and URLs without query strings only.

interface HttpLogger {
debug?(message: string, context: Readonly<Record<string, unknown>>): void;
warn?(message: string, context: Readonly<Record<string, unknown>>): void;
}

HttpRequestContext

Mutable request passed to request interceptors.

interface HttpRequestContext {
method: string;
url: string;
readonly headers: Headers;
body: BodyInit | null;
credentials: RequestCredentials;
cache: RequestCache | undefined;
redirect: RequestRedirect | undefined;
readonly signal: AbortSignal;
readonly attempt: number;
}

HttpRequestInfo

One network exchange of a call, as hooks see it.

interface HttpRequestInfo {
readonly call: HttpCallInfo;
readonly method: string;
readonly url: string;
readonly headers: Readonly<Record<string, string>>;
readonly attempt: number;
}

HttpResponse

Response data with metadata.

interface HttpResponse<D> {
readonly data: D;
readonly status: number;
readonly statusText: string;
readonly headers: Headers;
readonly url: string;
readonly fromCache: boolean;
readonly requestId: string | undefined;
}

InterceptorRegistry

Interceptors of one kind; use returns a function that removes the interceptor again.

interface InterceptorRegistry<F> {
use(interceptor: F): () => void;
clear(): void;
readonly size: number;
}

RequestInterceptors

Per-request interceptors, run after the client’s for requests and before them for responses and errors.

interface RequestInterceptors {
readonly request?: RequestInterceptor | readonly RequestInterceptor[];
readonly response?: ResponseInterceptor | readonly ResponseInterceptor[];
readonly error?: ErrorInterceptor | readonly ErrorInterceptor[];
}

RequestOptions

interface RequestOptions<T = unknown, R extends ResponseType = ResponseType> {
readonly query?: QueryInput;
readonly body?: unknown;
readonly headers?: HeadersInit;
readonly signal?: AbortSignal | undefined;
readonly responseType?: R;
readonly validate?: (data: unknown) => data is T;
readonly idempotencyKey?: boolean | string;
readonly expectStatus?: number | readonly number[] | ((status: number) => boolean);
readonly onDownloadProgress?: (progress: TransferProgress) => void;
readonly timeoutMs?: number;
readonly retry?: Partial<RetryPolicy> | false;
readonly cache?: RequestCache;
readonly credentials?: RequestCredentials;
readonly redirect?: RequestRedirect;
readonly dedupe?: boolean;
readonly etag?: boolean;
readonly group?: string;
readonly cancelKey?: string;
readonly interceptors?: RequestInterceptors;
readonly hooks?: HttpHooks;
}

ResponseTypeMap

Data type produced by each ResponseType; T is the JSON payload type.

interface ResponseTypeMap<T> {
json: T;
text: string;
blob: Blob;
arrayBuffer: ArrayBuffer;
stream: ReadableStream<Uint8Array>;
response: Response;
}

RetryPolicy

Retry policy. Only idempotent methods (or requests with an idempotency key) are retried.

interface RetryPolicy {
readonly retries: number;
readonly methods: readonly string[];
readonly statuses: readonly number[] | ((status: number) => boolean);
readonly network: boolean;
readonly respectRetryAfter: boolean;
readonly baseDelayMs: number;
readonly maxDelayMs: number;
}

XsrfOptions

Double-submit CSRF protection: copies a cookie into a header for same-origin unsafe methods.

interface XsrfOptions {
readonly cookieName?: string;
readonly headerName?: string;
}

ErrorInterceptor

Called with the final error (not for cancellations); returning a 2xx Response recovers.

type ErrorInterceptor = (error: Error, request: {
readonly method: string;
readonly url: string;
}) => Awaitable<Response | undefined>;

FetchLike

Minimal fetch signature; inject one for tests or SSR.

type FetchLike = (input: string, init: RequestInit) => Promise<Response>;

HttpOutcome

How a call ended.

type HttpOutcome = 'success' | 'error' | 'cancelled';

RequestInterceptor

Transforms the request: change it in place, return a replacement, or return a Response to skip the network.

type RequestInterceptor = (request: HttpRequestContext) => Awaitable<HttpRequestContext | Response | undefined>;

ResponseInterceptor

Transforms the response: may return a replacement Response.

type ResponseInterceptor = (response: Response, request: HttpRequestContext) => Awaitable<Response | undefined>;

ResponseType

How the response body is returned.

type ResponseType = 'json' | 'text' | 'blob' | 'arrayBuffer' | 'stream' | 'response';

isCancelled

True when error is a cancelled request (caller signal, cancelAll, cancelGroup, or a newer call with the same cancelKey). UIs usually ignore these.

function isCancelled(error: unknown): boolean;

Example:

import { HttpError } from '@absoluteui/helpers/error';
isCancelled(new HttpError('SVC_HTTP_ABORTED', 'The request was cancelled.', { status: 0, category: 'cancelled' })); // => true
isCancelled(new HttpError('SVC_HTTP_STATUS', 'Not found.', { status: 404 })); // => false

isHttpError

True when value is an HttpError, optionally with the given status.

function isHttpError(value: unknown, status?: number): value is HttpError;

Example:

import { HttpError } from '@absoluteui/helpers/error';
const e: unknown = new HttpError('SVC_HTTP_STATUS', 'Not found.', { status: 404 });
isHttpError(e, 404); // => true
isHttpError(e, 500); // => false
isHttpError(new Error('boom')); // => false

redactHeaders

Lowercased copy of headers with credential values replaced by '[REDACTED]', for logging.

function redactHeaders(headers: HeadersLike | undefined, extra?: readonly string[]): Record<string, string>;

Example:

redactHeaders({ Authorization: 'Bearer abc', Accept: 'application/json' });
// => { authorization: '[REDACTED]', accept: 'application/json' }

requestIdOf

Correlation id of a response: X-Request-Id, X-Correlation-Id, or the trace id of traceparent. Values that are not 1 to 128 visible ASCII characters are ignored.

function requestIdOf(headers: Headers): string | undefined;

Example:

requestIdOf(new Headers({ 'x-request-id': 'req-42' })); // => 'req-42'
requestIdOf(new Headers({ traceparent: '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' })); // => '4bf92f3577b34da6a3ce929d0e0e4736'
requestIdOf(new Headers()); // => undefined

toProblem

RFC 9457 problem details for an HttpError: the server’s document when it sent one, otherwise a synthesized about:blank problem. undefined for other errors.

function toProblem(error: unknown): ProblemDetails | undefined;

Example:

import { HttpError } from '@absoluteui/helpers/error';
const error = new HttpError('SVC_HTTP_STATUS', 'Check the highlighted fields.', { status: 422, fields: { email: 'Enter a valid email address.' } });
const p = toProblem(error);
p?.status; // => 422
p?.fieldErrors; // => { email: 'Enter a valid email address.' }
toProblem(new Error('boom')); // => undefined

SENSITIVE_HEADERS

Header names whose values redactHeaders always hides.

const SENSITIVE_HEADERS: readonly string[];

HeadersLike

Headers input accepted by redactHeaders.

type HeadersLike = Headers | readonly (readonly [string, string])[] | Readonly<Record<string, string | undefined>>;

TrustServerMessages

Decides whether a server-supplied message (Problem Details detail, or a top-level message string) replaces the catalog message. Default: never (server text may leak internals or be unlocalized).

type TrustServerMessages = boolean | ((info: {
readonly status: number;
readonly url: string;
readonly problem: ProblemDetails | undefined;
}) => boolean);

paginate

Iterates pages of a JSON collection. Stops at maxPages, at the last page, or when a next link or cursor repeats. Cross-origin next links throw SVC_HTTP_CROSS_ORIGIN_PAGE unless allowCrossOrigin.

function paginate<Item>(client: HttpClient, url: string, options: TypedPaginateOptions<Item>): AsyncGenerator<Page<Item>, void, undefined>;
function paginate(client: HttpClient, url: string, options: PaginateOptions & {
readonly items?: undefined;
}): AsyncGenerator<Page<unknown>, void, undefined>;

Example:

import { createMockFetch } from '@absoluteui/services/http-mock';
const fetch = createMockFetch([
{ url: '/users?page=2', body: [{ id: 3 }] },
{ url: '/users', body: [{ id: 1 }, { id: 2 }], headers: { link: '<http://localhost/users?page=2>; rel="next"' } },
]);
const api = createHttpClient({ fetch, baseUrl: 'http://localhost' });
const ids: number[] = [];
const toIds = (d: unknown): number[] => (Array.isArray(d) ? d.map((u: { id: number }) => u.id) : []);
for await (const page of paginate(api, '/users', { strategy: 'link', items: toIds })) ids.push(...page.items);
ids; // => [1, 2, 3]

Example:

import type { HttpClient } from '@absoluteui/services/http';
declare const api: HttpClient;
const nextCursor = (d: unknown) => (typeof d === 'object' && d !== null && 'next' in d && typeof d.next === 'string' ? d.next : null);
for await (const page of paginate(api, '/events', { strategy: 'cursor', getCursor: nextCursor })) console.log(page.index, page.items);

CursorPagination

Send the cursor from each page as a query parameter.

interface CursorPagination extends PaginateBase {
readonly strategy: 'cursor';
readonly cursorParam?: string;
readonly getCursor?: (data: unknown) => string | null | undefined;
}

LinkPagination

Follow rel="next" links from the Link header.

interface LinkPagination extends PaginateBase {
readonly strategy: 'link';
readonly allowCrossOrigin?: boolean;
}

OffsetPagination

Increase an offset by the page size until a short page or getTotal.

interface OffsetPagination extends PaginateBase {
readonly strategy: 'offset';
readonly pageSize?: number;
readonly offsetParam?: string;
readonly limitParam?: string;
readonly getTotal?: (data: unknown) => number | undefined;
}

Page

One fetched page.

interface Page<Item> {
readonly items: readonly Item[];
readonly data: unknown;
readonly index: number;
readonly status: number;
readonly headers: Headers;
}

PaginateOptions

type PaginateOptions = LinkPagination | CursorPagination | OffsetPagination;

TypedPaginateOptions

Pagination options with a typed item extractor.

type TypedPaginateOptions<Item> = PaginateOptions & {
readonly items: (data: unknown) => readonly Item[];
};

uploadWithProgress

Sends body with XMLHttpRequest and reports upload progress. Resolves { status, headers, body } for 2xx; rejects with an HttpError like createHttpClient: SVC_HTTP_STATUS with problem details, or status 0 SVC_HTTP_NETWORK (network/offline), SVC_HTTP_TIMEOUT (timeout), or SVC_HTTP_ABORTED (cancelled).

function uploadWithProgress<R extends XhrResponseType = 'json'>(url: string, body: XMLHttpRequestBodyInit | null, options?: UploadWithProgressOptions<R>): Promise<UploadResponse<XhrResponseTypeMap[R]>>;

Example:

const formData = new FormData();
formData.append('file', new File(['hello'], 'a.txt', { type: 'text/plain' }));
const bar = document.createElement('progress');
bar.max = 100;
const controller = new AbortController();
const res = await uploadWithProgress('/api/files', formData, { onProgress: (p) => { bar.value = p.percent ?? 0; }, signal: controller.signal });

UploadResponse

Result of uploadWithProgress.

interface UploadResponse<D> {
readonly status: number;
readonly headers: Headers;
readonly body: D;
}

UploadWithProgressOptions

interface UploadWithProgressOptions<R extends XhrResponseType = XhrResponseType> {
readonly method?: string;
readonly headers?: HeadersInit;
readonly signal?: AbortSignal | undefined;
readonly onProgress?: (progress: TransferProgress) => void;
readonly timeoutMs?: number;
readonly withCredentials?: boolean;
readonly responseType?: R;
readonly messages?: StatusMessageCatalog;
readonly trustServerMessages?: TrustServerMessages;
readonly json?: SafeJsonParseOptions;
readonly createXhr?: () => XMLHttpRequest;
}

XhrResponseTypeMap

Body type per XhrResponseType.

interface XhrResponseTypeMap {
json: unknown;
text: string;
blob: Blob;
arrayBuffer: ArrayBuffer;
}

XhrResponseType

Response body types supported by uploadWithProgress.

type XhrResponseType = 'json' | 'text' | 'blob' | 'arrayBuffer';