create-request 1.6.0 → 2.0.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/MIGRATION.md +158 -0
- package/README.md +422 -1292
- package/dist/index.cjs +730 -0
- package/dist/index.d.cts +851 -0
- package/dist/index.d.ts +851 -0
- package/dist/index.js +717 -0
- package/package.json +63 -67
- package/dist/library/BaseRequest.d.ts +0 -870
- package/dist/library/BodyRequest.d.ts +0 -101
- package/dist/library/RequestError.d.ts +0 -171
- package/dist/library/ResponseWrapper.d.ts +0 -182
- package/dist/library/apiBuilder.d.ts +0 -560
- package/dist/library/enums.d.ts +0 -85
- package/dist/library/index.cjs +0 -3160
- package/dist/library/index.cjs.map +0 -1
- package/dist/library/index.d.ts +0 -47
- package/dist/library/index.esm.js +0 -3140
- package/dist/library/index.esm.js.map +0 -1
- package/dist/library/index.esm.min.js +0 -1
- package/dist/library/index.esm.min.js.map +0 -1
- package/dist/library/index.min.cjs +0 -1
- package/dist/library/index.min.cjs.map +0 -1
- package/dist/library/requestFactories.d.ts +0 -91
- package/dist/library/requestMethods.d.ts +0 -91
- package/dist/library/types.d.ts +0 -329
- package/dist/library/utils/Config.d.ts +0 -221
- package/dist/library/utils/CookieUtils.d.ts +0 -9
- package/dist/library/utils/CsrfUtils.d.ts +0 -24
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,851 @@
|
|
|
1
|
+
//#region src/schema.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The Standard Schema interface (https://standardschema.dev), version 1.
|
|
4
|
+
*
|
|
5
|
+
* Any schema library that implements it — zod (≥ 3.24), valibot (≥ 1.0), arktype (≥ 2.0),
|
|
6
|
+
* effect/Schema and others — can be passed to `getJson(schema)`, `getData(schema)` and
|
|
7
|
+
* `getResult(schema)` to validate (and type) the response body without adding a dependency
|
|
8
|
+
* on any particular library.
|
|
9
|
+
*/
|
|
10
|
+
interface StandardSchemaV1<Input = unknown, Output = Input> {
|
|
11
|
+
/** The Standard Schema properties. */
|
|
12
|
+
readonly "~standard": StandardSchemaV1.Props<Input, Output>;
|
|
13
|
+
}
|
|
14
|
+
declare namespace StandardSchemaV1 {
|
|
15
|
+
/** The Standard Schema properties interface. */
|
|
16
|
+
interface Props<Input = unknown, Output = Input> {
|
|
17
|
+
/** The version number of the standard. */
|
|
18
|
+
readonly version: 1;
|
|
19
|
+
/** The vendor name of the schema library. */
|
|
20
|
+
readonly vendor: string;
|
|
21
|
+
/** Validates unknown input values. */
|
|
22
|
+
readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
|
|
23
|
+
/** Inferred types associated with the schema. */
|
|
24
|
+
readonly types?: Types<Input, Output> | undefined;
|
|
25
|
+
}
|
|
26
|
+
/** The result interface of the validate function. */
|
|
27
|
+
type Result<Output> = SuccessResult<Output> | FailureResult;
|
|
28
|
+
/** The result interface if validation succeeds. */
|
|
29
|
+
interface SuccessResult<Output> {
|
|
30
|
+
/** The typed output value. */
|
|
31
|
+
readonly value: Output;
|
|
32
|
+
/** The non-existent issues. */
|
|
33
|
+
readonly issues?: undefined;
|
|
34
|
+
}
|
|
35
|
+
/** The result interface if validation fails. */
|
|
36
|
+
interface FailureResult {
|
|
37
|
+
/** The issues of failed validation. */
|
|
38
|
+
readonly issues: readonly Issue[];
|
|
39
|
+
}
|
|
40
|
+
/** The issue interface of the failure output. */
|
|
41
|
+
interface Issue {
|
|
42
|
+
/** The error message of the issue. */
|
|
43
|
+
readonly message: string;
|
|
44
|
+
/** The path of the issue, if any. */
|
|
45
|
+
readonly path?: readonly (PropertyKey | PathSegment)[] | undefined;
|
|
46
|
+
}
|
|
47
|
+
/** The path segment interface of the issue. */
|
|
48
|
+
interface PathSegment {
|
|
49
|
+
/** The key representing a path segment. */
|
|
50
|
+
readonly key: PropertyKey;
|
|
51
|
+
}
|
|
52
|
+
/** The Standard Schema types interface. */
|
|
53
|
+
interface Types<Input = unknown, Output = Input> {
|
|
54
|
+
/** The input type of the schema. */
|
|
55
|
+
readonly input: Input;
|
|
56
|
+
/** The output type of the schema. */
|
|
57
|
+
readonly output: Output;
|
|
58
|
+
}
|
|
59
|
+
/** Infers the input type of a Standard Schema. */
|
|
60
|
+
type InferInput<Schema extends StandardSchemaV1> = NonNullable<Schema["~standard"]["types"]>["input"];
|
|
61
|
+
/** Infers the output type of a Standard Schema. */
|
|
62
|
+
type InferOutput<Schema extends StandardSchemaV1> = NonNullable<Schema["~standard"]["types"]>["output"];
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/error.d.ts
|
|
66
|
+
/** Everything a {@link RequestError} can carry besides its message. */
|
|
67
|
+
interface RequestErrorOptions {
|
|
68
|
+
/** What kind of failure this is — see {@link RequestErrorCode}. */
|
|
69
|
+
code: RequestErrorCode;
|
|
70
|
+
/** The URL that was requested (with query string). */
|
|
71
|
+
url: string;
|
|
72
|
+
/** The HTTP method that was used. */
|
|
73
|
+
method: Method;
|
|
74
|
+
/** HTTP status, when a response was received. */
|
|
75
|
+
status?: number | undefined;
|
|
76
|
+
/** The raw `Response`, when one was received. Its body has been read into `body` (unless it was larger than 1 MB). */
|
|
77
|
+
response?: Response | undefined;
|
|
78
|
+
/** The response body as text, when one was received and could be read (at most 1 MB). */
|
|
79
|
+
body?: string | undefined;
|
|
80
|
+
/** Schema issues, for `"VALIDATION"` errors coming from `getJson(schema)` and friends. */
|
|
81
|
+
issues?: readonly StandardSchemaV1.Issue[] | undefined;
|
|
82
|
+
/** The underlying error (what `fetch`, an interceptor or `JSON.parse` threw). */
|
|
83
|
+
cause?: unknown;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The single error type thrown by this library. Every rejection from `getResponse()`, `getJson()`, …
|
|
87
|
+
* is a `RequestError`, so one `instanceof` (or {@link isRequestError}) check is enough, and `code`
|
|
88
|
+
* tells you what happened.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```typescript
|
|
92
|
+
* try {
|
|
93
|
+
* await api.get("/users/42").getJson<User>();
|
|
94
|
+
* } catch (error) {
|
|
95
|
+
* if (!isRequestError(error)) throw error;
|
|
96
|
+
* switch (error.code) {
|
|
97
|
+
* case "HTTP": console.log(error.status, error.data); break; // parsed error body
|
|
98
|
+
* case "TIMEOUT": console.log("try again later"); break;
|
|
99
|
+
* case "ABORTED": break; // the user navigated away
|
|
100
|
+
* default: console.error(error.message, error.cause);
|
|
101
|
+
* }
|
|
102
|
+
* }
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export declare class RequestError<TData = unknown> extends Error {
|
|
106
|
+
readonly name = "RequestError";
|
|
107
|
+
/** What kind of failure this is — see {@link RequestErrorCode}. */
|
|
108
|
+
readonly code: RequestErrorCode;
|
|
109
|
+
/** The URL that was requested (with query string). */
|
|
110
|
+
readonly url: string;
|
|
111
|
+
/** The HTTP method that was used. */
|
|
112
|
+
readonly method: Method;
|
|
113
|
+
/** HTTP status, when a response was received. */
|
|
114
|
+
readonly status?: number | undefined;
|
|
115
|
+
/** The raw `Response`, when one was received. Its body has been read into `body` (unless it was larger than 1 MB). */
|
|
116
|
+
readonly response?: Response | undefined;
|
|
117
|
+
/** The response body as text, when one was received and could be read (at most 1 MB). */
|
|
118
|
+
readonly body?: string | undefined;
|
|
119
|
+
/** Schema issues, for `"VALIDATION"` errors coming from `getJson(schema)` and friends. */
|
|
120
|
+
readonly issues?: readonly StandardSchemaV1.Issue[] | undefined;
|
|
121
|
+
private _data?;
|
|
122
|
+
constructor(message: string, options: RequestErrorOptions);
|
|
123
|
+
/**
|
|
124
|
+
* `body` parsed as JSON — the shape most APIs use for error details.
|
|
125
|
+
* `undefined` when there is no body or it is not valid JSON; never throws.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```typescript
|
|
129
|
+
* catch (error) {
|
|
130
|
+
* if (isRequestError(error)) console.log(error.data?.message ?? error.message);
|
|
131
|
+
* }
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
get data(): TData | undefined;
|
|
135
|
+
/** Shorthand for `code === "TIMEOUT"`. */
|
|
136
|
+
get isTimeout(): boolean;
|
|
137
|
+
/** Shorthand for `code === "ABORTED"`. */
|
|
138
|
+
get isAborted(): boolean;
|
|
139
|
+
}
|
|
140
|
+
/** Type guard for {@link RequestError} — handy in `catch (error: unknown)` blocks. */
|
|
141
|
+
export declare const isRequestError: (error: unknown) => error is RequestError;
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region src/types.d.ts
|
|
144
|
+
/** The HTTP methods a request can be created with. */
|
|
145
|
+
type Method = "GET" | "HEAD" | "OPTIONS" | "DELETE" | "POST" | "PUT" | "PATCH";
|
|
146
|
+
/** The HTTP methods that may carry a request body (`withBody` / `withGraphQL` are only available on these). */
|
|
147
|
+
type BodyMethod = "POST" | "PUT" | "PATCH" | "DELETE";
|
|
148
|
+
/** What `fetch` accepts as a body. */
|
|
149
|
+
type FetchBody = NonNullable<RequestInit["body"]>;
|
|
150
|
+
/** `"include"`, `"omit"` or `"same-origin"`. */
|
|
151
|
+
type CredentialsMode = NonNullable<RequestInit["credentials"]>;
|
|
152
|
+
/** `"cors"`, `"no-cors"`, `"same-origin"` or `"navigate"`. */
|
|
153
|
+
type CorsMode = NonNullable<RequestInit["mode"]>;
|
|
154
|
+
/** `"follow"`, `"error"` or `"manual"`. */
|
|
155
|
+
type RedirectMode = NonNullable<RequestInit["redirect"]>;
|
|
156
|
+
/** A referrer policy name (`"no-referrer"`, `"strict-origin-when-cross-origin"`, …). */
|
|
157
|
+
type ReferrerPolicyName = NonNullable<RequestInit["referrerPolicy"]>;
|
|
158
|
+
/** How the HTTP cache is used. */
|
|
159
|
+
type CacheMode = "default" | "force-cache" | "no-cache" | "no-store" | "only-if-cached" | "reload";
|
|
160
|
+
/** A fetch priority hint. */
|
|
161
|
+
type PriorityHint = "auto" | "high" | "low";
|
|
162
|
+
/**
|
|
163
|
+
* Anything `withBody()` accepts.
|
|
164
|
+
*
|
|
165
|
+
* - `string` → sent as-is (`Content-Type: text/plain` unless you set one)
|
|
166
|
+
* - `Blob` / `File`, `FormData`, `URLSearchParams` → sent as-is, `fetch` sets the matching `Content-Type`
|
|
167
|
+
* - `ArrayBuffer`, typed arrays, `ReadableStream` → sent as-is, no `Content-Type` unless you set one
|
|
168
|
+
* - any other JSON-serialisable object or array → `JSON.stringify`-ed (`Content-Type: application/json` unless you set one)
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* ```typescript
|
|
172
|
+
* request.withBody({ name: "Ada" }); // JSON
|
|
173
|
+
* request.withBody(new FormData(form)); // multipart
|
|
174
|
+
* request.withBody("id=1&name=Ada"); // text/plain — set withContentType() for form-urlencoded
|
|
175
|
+
* ```
|
|
176
|
+
*/
|
|
177
|
+
type Body = FetchBody | object;
|
|
178
|
+
/** A single query-string value. Arrays produce repeated keys (`?tag=a&tag=b`); `null`/`undefined` remove the key. */
|
|
179
|
+
type QueryValue = string | number | boolean | Date | null | undefined | readonly (string | number | boolean | Date)[];
|
|
180
|
+
/** Query parameters as a plain object (`{ page: 1, tags: ["a", "b"] }`) or as `URLSearchParams`. */
|
|
181
|
+
type QueryParams = Record<string, QueryValue> | URLSearchParams;
|
|
182
|
+
/** Headers as a plain object. `null`/`undefined` unsets the header (useful to drop an api-level default). */
|
|
183
|
+
type HeadersRecord = Record<string, string | number | null | undefined>;
|
|
184
|
+
/** Cookies as a plain object of `name → value`. Values are sent verbatim (no encoding). */
|
|
185
|
+
type CookiesRecord = Record<string, string>;
|
|
186
|
+
/** A fetch-compatible function: receives the final URL and `RequestInit` and returns a `Response`. */
|
|
187
|
+
type FetchFunction = (url: string, init: RequestInit) => Promise<Response>;
|
|
188
|
+
/**
|
|
189
|
+
* Discriminates the kind of failure a {@link RequestError} represents.
|
|
190
|
+
*
|
|
191
|
+
* - `"HTTP"` — the server answered with a non-2xx status (`status`, `response`, `body`, `data` are set)
|
|
192
|
+
* - `"NETWORK"` — `fetch` itself rejected: DNS, connection refused, CORS, offline, a relative URL in Node.js
|
|
193
|
+
* (`cause` is the original error)
|
|
194
|
+
* - `"TIMEOUT"` — the `withTimeout()` deadline passed, or a signal aborted with a `TimeoutError` (`AbortSignal.timeout()`)
|
|
195
|
+
* - `"ABORTED"` — a signal passed with `withSignal()` / `withAbortController()` was aborted
|
|
196
|
+
* - `"PARSE"` — the body could not be read or parsed (invalid JSON, body already consumed, selector threw)
|
|
197
|
+
* - `"VALIDATION"` — the response did not match the schema passed to `getJson(schema)` (`issues` is set),
|
|
198
|
+
* or the request could not be built: an empty or unparsable absolute URL, an invalid header, timeout,
|
|
199
|
+
* retries or body (those last three are thrown synchronously by the `with*` method)
|
|
200
|
+
* - `"INTERCEPTOR"` — an interceptor or callback (retry, CSRF token) threw (`cause` is what it threw)
|
|
201
|
+
* - `"GRAPHQL"` — the GraphQL response contained `errors` and `throwOnError` was enabled
|
|
202
|
+
*/
|
|
203
|
+
type RequestErrorCode = "HTTP" | "NETWORK" | "TIMEOUT" | "ABORTED" | "PARSE" | "VALIDATION" | "INTERCEPTOR" | "GRAPHQL";
|
|
204
|
+
/** What a retry decision or delay function receives. */
|
|
205
|
+
interface RetryContext {
|
|
206
|
+
/** The retry about to happen, starting at 1 (so `attempt === 1` follows the first failure). */
|
|
207
|
+
attempt: number;
|
|
208
|
+
/** The error that triggered this retry. */
|
|
209
|
+
error: RequestError;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Configuration for automatic retries — see `withRetries()`.
|
|
213
|
+
*
|
|
214
|
+
* By default a request is retried after a network error, a timeout, or a response with status
|
|
215
|
+
* 408, 425, 429, 500, 502, 503 or 504, with exponential backoff (300 ms, 600 ms, 1.2 s, … plus
|
|
216
|
+
* up to 100 ms of jitter, capped at `maxDelay`). A `Retry-After` header is honoured when present.
|
|
217
|
+
* Aborted requests and requests with a `ReadableStream` body are never retried, whatever the policy.
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* ```typescript
|
|
221
|
+
* request.withRetries(3); // 3 retries, default policy
|
|
222
|
+
* request.withRetries({ attempts: 3, delay: 1000 }); // fixed 1 s delay
|
|
223
|
+
* request.withRetries({ attempts: 5, methods: ["GET", "HEAD"] }); // idempotent methods only
|
|
224
|
+
* request.withRetries({
|
|
225
|
+
* attempts: 3,
|
|
226
|
+
* delay: ({ attempt }) => attempt * 500,
|
|
227
|
+
* shouldRetry: ({ error }) => error.code === "HTTP" && error.status === 503,
|
|
228
|
+
* onRetry: ({ attempt, delay }) => console.log(`retry #${attempt} in ${delay}ms`),
|
|
229
|
+
* });
|
|
230
|
+
* ```
|
|
231
|
+
*/
|
|
232
|
+
interface RetryConfig {
|
|
233
|
+
/** Number of retries after the first attempt (`3` means up to 4 requests in total). */
|
|
234
|
+
attempts: number;
|
|
235
|
+
/**
|
|
236
|
+
* Delay before each retry, in milliseconds, or a function computing it.
|
|
237
|
+
* When set, it takes precedence over a `Retry-After` header. Default: exponential backoff.
|
|
238
|
+
*/
|
|
239
|
+
delay?: number | ((context: RetryContext) => number) | undefined;
|
|
240
|
+
/** Statuses that are retried. Default: `[408, 425, 429, 500, 502, 503, 504]`. Network errors and timeouts are retried by default. */
|
|
241
|
+
statuses?: readonly number[] | undefined;
|
|
242
|
+
/** Methods that are retried. Default: all. Use `["GET", "HEAD", "OPTIONS", "PUT", "DELETE"]` to retry idempotent requests only. */
|
|
243
|
+
methods?: readonly Method[] | undefined;
|
|
244
|
+
/**
|
|
245
|
+
* Upper bound, in milliseconds, for the default backoff. A `Retry-After` header longer than this
|
|
246
|
+
* cancels the retry so you can react to it yourself. Default: `30000`.
|
|
247
|
+
*/
|
|
248
|
+
maxDelay?: number | undefined;
|
|
249
|
+
/** Full override of the retry decision (`statuses` and `methods` are ignored). Aborts and stream bodies still never retry. */
|
|
250
|
+
shouldRetry?: ((context: RetryContext) => boolean | Promise<boolean>) | undefined;
|
|
251
|
+
/** Called before every retry, after the delay has been computed. Awaited if it returns a promise. */
|
|
252
|
+
onRetry?: ((context: RetryContext & {
|
|
253
|
+
delay: number;
|
|
254
|
+
}) => void | Promise<void>) | undefined;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* The request as it is about to be sent, handed to request interceptors.
|
|
258
|
+
*
|
|
259
|
+
* `headers` keys are lower-case; `body` is already serialised (JSON bodies are strings);
|
|
260
|
+
* `signal` combines the signals passed with `withSignal()` (the timeout is added after interceptors ran).
|
|
261
|
+
* Mutate it in place or return a new object; return a `Response` to skip the network entirely.
|
|
262
|
+
*/
|
|
263
|
+
interface RequestConfig extends Omit<RequestInit, "headers" | "body" | "method" | "signal" | "window"> {
|
|
264
|
+
/** The final URL, with the query string and, for api requests, the base URL applied. */
|
|
265
|
+
url: string;
|
|
266
|
+
/** The HTTP method. */
|
|
267
|
+
method: Method;
|
|
268
|
+
/** The headers to send, with lower-case names. The CSRF header (`withCsrf()`) is added after interceptors ran. */
|
|
269
|
+
headers: Record<string, string>;
|
|
270
|
+
/** The serialised body: JSON bodies are already strings. */
|
|
271
|
+
body?: FetchBody | null | undefined;
|
|
272
|
+
/** The signals passed with `withSignal()` / `withAbortController()`, combined. The timeout is added after interceptors ran. */
|
|
273
|
+
signal?: AbortSignal | undefined;
|
|
274
|
+
/** Required by browsers and Node for `ReadableStream` bodies; set automatically. */
|
|
275
|
+
duplex?: "half";
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Runs before the request is sent. Return nothing to keep your in-place changes, a new
|
|
279
|
+
* {@link RequestConfig} to replace it, or a `Response` to short-circuit the request
|
|
280
|
+
* (it is then treated like a fetched response: status is checked, response interceptors run —
|
|
281
|
+
* but `withTimeout()` does not apply to it, since nothing was fetched).
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* ```typescript
|
|
285
|
+
* const withTraceId: RequestInterceptor = config => {
|
|
286
|
+
* config.headers["x-trace-id"] = crypto.randomUUID();
|
|
287
|
+
* };
|
|
288
|
+
* ```
|
|
289
|
+
*/
|
|
290
|
+
type RequestInterceptor = (config: RequestConfig) => RequestConfig | Response | void | Promise<RequestConfig | Response | void>;
|
|
291
|
+
/**
|
|
292
|
+
* Runs after a successful response (before the body is read), with the request that produced it.
|
|
293
|
+
* Return nothing to keep the response or another {@link ResponseWrapper} to replace it.
|
|
294
|
+
*
|
|
295
|
+
* @example
|
|
296
|
+
* ```typescript
|
|
297
|
+
* const logStatus: ResponseInterceptor = (response, request) => {
|
|
298
|
+
* console.log(request.method, response.url, response.status);
|
|
299
|
+
* };
|
|
300
|
+
* ```
|
|
301
|
+
*/
|
|
302
|
+
type ResponseInterceptor = (response: ResponseWrapper, request: HttpRequest) => ResponseWrapper | void | Promise<ResponseWrapper | void>;
|
|
303
|
+
/**
|
|
304
|
+
* Runs once when the request has failed for good (after all retries), with the request that failed.
|
|
305
|
+
* Return nothing to keep the error, another {@link RequestError} to replace it, or a
|
|
306
|
+
* {@link ResponseWrapper} to recover — typically by replaying `request.clone()`. Throwing replaces the error as well.
|
|
307
|
+
*
|
|
308
|
+
* @example
|
|
309
|
+
* ```typescript
|
|
310
|
+
* const replayed = new WeakSet<HttpRequest>();
|
|
311
|
+
* const refreshOn401: ErrorInterceptor = async (error, request) => {
|
|
312
|
+
* if (error.status !== 401 || replayed.has(request)) return;
|
|
313
|
+
* token = await refreshToken(); // the request interceptor that adds the token reads it
|
|
314
|
+
* const retry = request.clone();
|
|
315
|
+
* replayed.add(retry); // never loop on a persistent 401
|
|
316
|
+
* return retry.getResponse();
|
|
317
|
+
* };
|
|
318
|
+
* ```
|
|
319
|
+
*/
|
|
320
|
+
type ErrorInterceptor = (error: RequestError, request: HttpRequest) => RequestError | ResponseWrapper | void | Promise<RequestError | ResponseWrapper | void>;
|
|
321
|
+
/** Options for `withGraphQL()`. */
|
|
322
|
+
interface GraphQLOptions {
|
|
323
|
+
/** Throw a `RequestError` with code `"GRAPHQL"` when the response contains a non-empty `errors` array. Default: `false`. */
|
|
324
|
+
throwOnError?: boolean | undefined;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Options for `withCsrf()`.
|
|
328
|
+
*
|
|
329
|
+
* The token is attached only to same-origin requests (evaluated against the final URL, after
|
|
330
|
+
* interceptors, before any redirect) unless `crossOrigin` is set. Outside a browser there is no
|
|
331
|
+
* page origin and every request counts as same-origin.
|
|
332
|
+
*/
|
|
333
|
+
interface CsrfOptions {
|
|
334
|
+
/** Cookie to read the token from (browser only). Default: `"XSRF-TOKEN"`. Ignored when `token` is set. */
|
|
335
|
+
cookie?: string | undefined;
|
|
336
|
+
/** Header the token is sent in. Default: `"X-XSRF-TOKEN"` when read from a cookie, `"X-CSRF-Token"` when `token` is set. */
|
|
337
|
+
header?: string | undefined;
|
|
338
|
+
/** A token, or a function returning one per request (return `null`/`undefined` to send nothing). */
|
|
339
|
+
token?: string | (() => string | null | undefined) | undefined;
|
|
340
|
+
/** Also attach the token to cross-origin URLs. Default: `false`. */
|
|
341
|
+
crossOrigin?: boolean | undefined;
|
|
342
|
+
}
|
|
343
|
+
/** What `getResult()` resolves to: `error` is `null` on success and the {@link RequestError} otherwise (then `data` is `null`). */
|
|
344
|
+
type RequestResult<T> = {
|
|
345
|
+
data: T;
|
|
346
|
+
error: null;
|
|
347
|
+
} | {
|
|
348
|
+
data: null;
|
|
349
|
+
error: RequestError;
|
|
350
|
+
};
|
|
351
|
+
//#endregion
|
|
352
|
+
//#region src/response.d.ts
|
|
353
|
+
/**
|
|
354
|
+
* A successful response. Exposes the status line and headers, and reads the body in the format you
|
|
355
|
+
* ask for. The body is buffered once, so you can call several readers, in any order and concurrently
|
|
356
|
+
* (`getJson()` after `getText()`, `Promise.all([res.getJson(), res.getBlob()])`, …) — except
|
|
357
|
+
* `getBody()`, which hands you the live stream and therefore excludes the others.
|
|
358
|
+
*
|
|
359
|
+
* `T` is the JSON type declared at the request (`api.get<User>()`) and is the default for `getJson()` / `getData()`.
|
|
360
|
+
*
|
|
361
|
+
* @example
|
|
362
|
+
* ```typescript
|
|
363
|
+
* const res = await api.get<User>("/me").getResponse();
|
|
364
|
+
* console.log(res.status, res.headers.get("etag"));
|
|
365
|
+
* const user = await res.getJson(); // User
|
|
366
|
+
* ```
|
|
367
|
+
*/
|
|
368
|
+
export declare class ResponseWrapper<T = unknown> {
|
|
369
|
+
/** The underlying `Response`. Read its body through this wrapper's methods, or take it over with `getBody()`. */
|
|
370
|
+
readonly raw: Response;
|
|
371
|
+
/** The URL that was requested, including the query string (after interceptors). */
|
|
372
|
+
readonly url: string;
|
|
373
|
+
/** The HTTP method that was used. */
|
|
374
|
+
readonly method: Method;
|
|
375
|
+
private _bin?;
|
|
376
|
+
private _text?;
|
|
377
|
+
private _json?;
|
|
378
|
+
/** Wraps a `Response` — useful to hand a synthetic response to an error interceptor. */
|
|
379
|
+
constructor(
|
|
380
|
+
/** The underlying `Response`. Read its body through this wrapper's methods, or take it over with `getBody()`. */
|
|
381
|
+
raw: Response,
|
|
382
|
+
/** The URL that was requested, including the query string (after interceptors). */
|
|
383
|
+
url?: string,
|
|
384
|
+
/** The HTTP method that was used. */
|
|
385
|
+
method?: Method);
|
|
386
|
+
/** HTTP status code (`200`, `404`, …). */
|
|
387
|
+
get status(): number;
|
|
388
|
+
/** HTTP status text (`"OK"`, `"Not Found"`, …); empty over HTTP/2. */
|
|
389
|
+
get statusText(): string;
|
|
390
|
+
/** Whether the status is in the 200–299 range. */
|
|
391
|
+
get ok(): boolean;
|
|
392
|
+
/** Response headers. */
|
|
393
|
+
get headers(): Headers;
|
|
394
|
+
private _err;
|
|
395
|
+
/** The error for a body that cannot be read: TIMEOUT / ABORTED when the deadline or a signal fired, otherwise PARSE with `fallback`. */
|
|
396
|
+
private _unreadable;
|
|
397
|
+
private _buffer;
|
|
398
|
+
private _validate;
|
|
399
|
+
/** The body as an `ArrayBuffer`. */
|
|
400
|
+
getArrayBuffer(): Promise<ArrayBuffer>;
|
|
401
|
+
/** The body as a `Blob`, typed with the response's `Content-Type`. */
|
|
402
|
+
getBlob(): Promise<Blob>;
|
|
403
|
+
/** The body decoded as UTF-8 text (`""` for an empty body). */
|
|
404
|
+
getText(): Promise<string>;
|
|
405
|
+
/** The body parsed as `multipart/form-data` or `application/x-www-form-urlencoded`. */
|
|
406
|
+
getFormData(): Promise<FormData>;
|
|
407
|
+
/**
|
|
408
|
+
* The raw body stream, for streaming consumption (downloads with progress, SSE/NDJSON, LLM output…).
|
|
409
|
+
* Unlike the other readers it is not buffered: the body can be read once, and only if no other reader ran.
|
|
410
|
+
* Taking the stream also ends the request's `withTimeout()` deadline — from here on the stream is yours.
|
|
411
|
+
*
|
|
412
|
+
* @example
|
|
413
|
+
* ```typescript
|
|
414
|
+
* const reader = (await request.getBody())!.pipeThrough(new TextDecoderStream()).getReader();
|
|
415
|
+
* for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) process(chunk.value);
|
|
416
|
+
* ```
|
|
417
|
+
*/
|
|
418
|
+
getBody(): ReadableStream<Uint8Array> | null;
|
|
419
|
+
/**
|
|
420
|
+
* The body parsed as JSON. An empty body (`204`, `Content-Length: 0`, whitespace) yields `null` —
|
|
421
|
+
* declare it in the type (`getJson<User | null>()`) for endpoints that may return nothing.
|
|
422
|
+
*
|
|
423
|
+
* Pass a [Standard Schema](https://standardschema.dev) (zod, valibot, arktype, …) to validate the body
|
|
424
|
+
* and infer its type; a mismatch rejects with a `RequestError` whose `code` is `"VALIDATION"` and
|
|
425
|
+
* whose `issues` lists what went wrong.
|
|
426
|
+
*
|
|
427
|
+
* @example
|
|
428
|
+
* ```typescript
|
|
429
|
+
* const user = await res.getJson<User>();
|
|
430
|
+
* const user = await res.getJson(UserSchema); // typed from the schema, validated at runtime
|
|
431
|
+
* ```
|
|
432
|
+
*/
|
|
433
|
+
getJson<U = T>(): Promise<U>;
|
|
434
|
+
getJson<S extends StandardSchemaV1>(schema: S): Promise<StandardSchemaV1.InferOutput<S>>;
|
|
435
|
+
/**
|
|
436
|
+
* `getJson()` followed by a selector, so you can pick the part you need in one call. The selector
|
|
437
|
+
* receives `T` (declare it on the request: `api.get<Page>()`), or pass both type arguments explicitly.
|
|
438
|
+
* Errors thrown by the selector are reported as a `RequestError` with code `"PARSE"`. A schema can be validated first.
|
|
439
|
+
*
|
|
440
|
+
* @example
|
|
441
|
+
* ```typescript
|
|
442
|
+
* const names = await res.getData(page => page.users.map(u => u.name)); // res: ResponseWrapper<Page>
|
|
443
|
+
* const names = await res.getData<Page, string[]>(page => page.users.map(u => u.name));
|
|
444
|
+
* const names = await res.getData(PageSchema, page => page.users.map(u => u.name));
|
|
445
|
+
* ```
|
|
446
|
+
*/
|
|
447
|
+
getData<U = T>(): Promise<U>;
|
|
448
|
+
getData<S extends StandardSchemaV1>(schema: S): Promise<StandardSchemaV1.InferOutput<S>>;
|
|
449
|
+
getData<S extends StandardSchemaV1, R>(schema: S, selector: (data: StandardSchemaV1.InferOutput<S>) => R): Promise<R>;
|
|
450
|
+
getData<R>(selector: (data: T) => R): Promise<R>;
|
|
451
|
+
getData<U, R>(selector: (data: U) => R): Promise<R>;
|
|
452
|
+
}
|
|
453
|
+
//#endregion
|
|
454
|
+
//#region src/request.d.ts
|
|
455
|
+
/**
|
|
456
|
+
* A request under construction. Configure it with the `with*` methods (each returns the same request,
|
|
457
|
+
* so calls chain), then execute it with one of the `get*` methods. Nothing is sent until you execute.
|
|
458
|
+
*
|
|
459
|
+
* `M` is the HTTP method — `withBody()` and `withGraphQL()` only exist for methods that carry a body —
|
|
460
|
+
* and `T` is the JSON type the response is expected to have (`api.get<User>("/me")`), used as the
|
|
461
|
+
* default for `getJson()` / `getData()` / `getResult()`.
|
|
462
|
+
*
|
|
463
|
+
* Create requests with `create.get(url)`, `create.post(url)`, … or through an api instance; the class
|
|
464
|
+
* is exported for type annotations and `instanceof` checks.
|
|
465
|
+
*
|
|
466
|
+
* @example
|
|
467
|
+
* ```typescript
|
|
468
|
+
* const user = await create
|
|
469
|
+
* .post("https://api.example.com/users")
|
|
470
|
+
* .withBearerToken(token)
|
|
471
|
+
* .withBody({ name: "Ada" })
|
|
472
|
+
* .withTimeout(5000)
|
|
473
|
+
* .withRetries(2)
|
|
474
|
+
* .getJson<User>();
|
|
475
|
+
* ```
|
|
476
|
+
*/
|
|
477
|
+
export declare class HttpRequest<M extends Method = Method, T = unknown> {
|
|
478
|
+
/** The HTTP method. */
|
|
479
|
+
readonly method: M;
|
|
480
|
+
private readonly _url;
|
|
481
|
+
/** Prefer `create.get(url)`, `create.post(url)`, … or an api instance to construct requests. */
|
|
482
|
+
constructor(
|
|
483
|
+
/** The HTTP method. */
|
|
484
|
+
method: M, url: string);
|
|
485
|
+
/** The URL this request will be sent to, including the query string. */
|
|
486
|
+
get url(): string;
|
|
487
|
+
private _fail;
|
|
488
|
+
private _set;
|
|
489
|
+
/**
|
|
490
|
+
* Sets several headers at once. Names are case-insensitive (stored lower-case); a `null` or `undefined`
|
|
491
|
+
* value removes the header, which is how a request drops a default set on its api.
|
|
492
|
+
*
|
|
493
|
+
* @example
|
|
494
|
+
* ```typescript
|
|
495
|
+
* request.withHeaders({ Accept: "application/json", "X-Request-Id": id });
|
|
496
|
+
* api.get("/public").withHeaders({ Authorization: null }); // send this one unauthenticated
|
|
497
|
+
* ```
|
|
498
|
+
*/
|
|
499
|
+
withHeaders<H extends { [K in keyof H]: string | number | null | undefined; }>(headers: H): this;
|
|
500
|
+
/** Sets one header — see {@link withHeaders}. */
|
|
501
|
+
withHeader(name: string, value: string | number | null | undefined): this;
|
|
502
|
+
/**
|
|
503
|
+
* Sets the `Content-Type` header. Rarely needed: JSON and text bodies set it automatically and
|
|
504
|
+
* `FormData` / `Blob` bodies must leave it to `fetch` (it adds the multipart boundary).
|
|
505
|
+
*/
|
|
506
|
+
withContentType(contentType: string): this;
|
|
507
|
+
/** Sets the `Authorization` header verbatim (`"Bearer …"`, `"Basic …"`, a custom scheme). */
|
|
508
|
+
withAuthorization(value: string): this;
|
|
509
|
+
/** Sets `Authorization: Bearer <token>`. */
|
|
510
|
+
withBearerToken(token: string): this;
|
|
511
|
+
/** Sets `Authorization: Basic <base64(username:password)>` (UTF-8 safe). */
|
|
512
|
+
withBasicAuth(username: string, password: string): this;
|
|
513
|
+
/**
|
|
514
|
+
* Adds cookies to the `Cookie` header. Names and values are sent verbatim — encode them yourself if
|
|
515
|
+
* needed, and never pass untrusted values (a `;` would smuggle in another cookie). **Node.js /
|
|
516
|
+
* server-side only**: browsers ignore a `Cookie` header on `fetch` and send their own cookies
|
|
517
|
+
* instead — use `withCredentials("include")` there.
|
|
518
|
+
*/
|
|
519
|
+
withCookies<C extends { [K in keyof C]: string; }>(cookies: C): this;
|
|
520
|
+
/** Adds one cookie — see {@link withCookies}. */
|
|
521
|
+
withCookie(name: string, value: string): this;
|
|
522
|
+
/**
|
|
523
|
+
* Enables CSRF protection: a token is attached to requests whose URL (final, after interceptors) is
|
|
524
|
+
* same-origin with the page, so it is not sent to third-party hosts. By default the token is read
|
|
525
|
+
* from the `XSRF-TOKEN` cookie and sent as `X-XSRF-TOKEN` — the convention of Angular, Laravel and
|
|
526
|
+
* Spring. See {@link CsrfOptions} for other setups.
|
|
527
|
+
*
|
|
528
|
+
* Two limits of that guarantee: the check happens before `fetch` runs, so a same-origin URL that
|
|
529
|
+
* *redirects* to another origin still carries the header (`fetch` only strips `Authorization` on
|
|
530
|
+
* cross-origin redirects) — use `withRedirect("error")` for endpoints that may redirect elsewhere;
|
|
531
|
+
* and outside a browser there is no page origin, so every URL counts as same-origin.
|
|
532
|
+
*
|
|
533
|
+
* @example
|
|
534
|
+
* ```typescript
|
|
535
|
+
* api.withCsrf(); // XSRF-TOKEN cookie → X-XSRF-TOKEN
|
|
536
|
+
* api.withCsrf({ cookie: "csrftoken", header: "X-CSRFToken" }); // Django
|
|
537
|
+
* api.withCsrf({ token: () => document.querySelector("meta[name=csrf-token]")?.getAttribute("content") }); // Rails
|
|
538
|
+
* ```
|
|
539
|
+
*/
|
|
540
|
+
withCsrf(options?: CsrfOptions): this;
|
|
541
|
+
/**
|
|
542
|
+
* Sends a CSRF token you already hold in the given header (default `X-CSRF-Token`) — with every request,
|
|
543
|
+
* whatever its origin. It is a plain header; prefer {@link withCsrf} (`{ token }`) to keep the same-origin check.
|
|
544
|
+
*/
|
|
545
|
+
withCsrfToken(token: string, header?: string): this;
|
|
546
|
+
/**
|
|
547
|
+
* Sets query parameters. A key that is already present is replaced (so a request can override an
|
|
548
|
+
* api-level default); arrays produce repeated keys, `Date`s are sent as ISO strings, and
|
|
549
|
+
* `null` / `undefined` remove the key. Accepts a `URLSearchParams` too.
|
|
550
|
+
*
|
|
551
|
+
* @example
|
|
552
|
+
* ```typescript
|
|
553
|
+
* request.withQueryParams({ page: 2, tags: ["a", "b"], since: new Date() });
|
|
554
|
+
* // ?page=2&tags=a&tags=b&since=2026-01-01T00%3A00%3A00.000Z
|
|
555
|
+
* ```
|
|
556
|
+
*/
|
|
557
|
+
withQueryParams<P extends { [K in keyof P]: QueryValue; }>(params: P | URLSearchParams): this;
|
|
558
|
+
/** Sets one query parameter — see {@link withQueryParams}. */
|
|
559
|
+
withQueryParam(key: string, value: QueryValue): this;
|
|
560
|
+
/**
|
|
561
|
+
* Fails the request with code `"TIMEOUT"` if it has not completed — response received *and* body read —
|
|
562
|
+
* within `ms` milliseconds. The clock starts after request interceptors ran and applies to each attempt
|
|
563
|
+
* when retries are enabled; `getBody()` ends the deadline and hands you the stream. It does not cover
|
|
564
|
+
* a `Response` returned by a request interceptor (nothing was fetched).
|
|
565
|
+
* `0`, `Infinity` or anything above 2^31 − 1 ms (≈ 24.8 days, the limit of `setTimeout`) remove a
|
|
566
|
+
* timeout set earlier (for instance by an api instance).
|
|
567
|
+
*/
|
|
568
|
+
withTimeout(ms: number): this;
|
|
569
|
+
/**
|
|
570
|
+
* Retries failed requests. Pass a number of retries for the default policy (network errors, timeouts,
|
|
571
|
+
* 408/425/429/500/502/503/504, exponential backoff, `Retry-After` honoured) or a {@link RetryConfig}
|
|
572
|
+
* to tune it. Calling it again merges with the previous configuration.
|
|
573
|
+
*/
|
|
574
|
+
withRetries(retries: number | RetryConfig): this;
|
|
575
|
+
/** Called before every retry — shorthand for `withRetries({ onRetry })`. It does not enable retries by itself. */
|
|
576
|
+
onRetry(callback: RetryConfig["onRetry"]): this;
|
|
577
|
+
/**
|
|
578
|
+
* Cancels the request when `signal` aborts (error code `"ABORTED"`). Call it several times to
|
|
579
|
+
* combine signals — for instance the one your data-fetching library hands you and your own.
|
|
580
|
+
*
|
|
581
|
+
* @example
|
|
582
|
+
* ```typescript
|
|
583
|
+
* useQuery({ queryKey: ["user", id], queryFn: ({ signal }) => api.get(`/users/${id}`).withSignal(signal).getJson<User>() });
|
|
584
|
+
* ```
|
|
585
|
+
*/
|
|
586
|
+
withSignal(signal: AbortSignal): this;
|
|
587
|
+
/** Cancels the request when `controller.abort()` is called — shorthand for `withSignal(controller.signal)`. */
|
|
588
|
+
withAbortController(controller: AbortController): this;
|
|
589
|
+
/**
|
|
590
|
+
* Uses `fetchFn` instead of the global `fetch`: inject a stub in tests, an undici `Agent` or proxy in
|
|
591
|
+
* Node.js, or a framework's patched fetch (Next.js caching options). It receives the final URL and
|
|
592
|
+
* `RequestInit` and must honour `init.signal` for timeouts and cancellation to work.
|
|
593
|
+
*
|
|
594
|
+
* @example
|
|
595
|
+
* ```typescript
|
|
596
|
+
* request.withFetch(async () => new Response('{"ok":true}')); // test stub
|
|
597
|
+
* request.withFetch((url, init) => undiciFetch(url, { ...init, dispatcher: agent })); // undici agent
|
|
598
|
+
* request.withFetch((url, init) => fetch(url, { ...init, next: { revalidate: 60 } })); // Next.js
|
|
599
|
+
* ```
|
|
600
|
+
*/
|
|
601
|
+
withFetch(fetchFn: FetchFunction): this;
|
|
602
|
+
/** Whether cookies and auth headers are sent cross-origin: `"include"`, `"omit"` or `"same-origin"` (browser default). */
|
|
603
|
+
withCredentials(credentials: CredentialsMode): this;
|
|
604
|
+
/** CORS mode: `"cors"` (default), `"no-cors"` (opaque response, status 0 — not treated as an error) or `"same-origin"`. */
|
|
605
|
+
withMode(mode: CorsMode): this;
|
|
606
|
+
/**
|
|
607
|
+
* Redirect handling: `"follow"` (default), `"error"` (rejects with a network error) or `"manual"`, which
|
|
608
|
+
* resolves with the redirect itself — an opaque response (status 0) in browsers, the actual 3xx in Node.js.
|
|
609
|
+
*/
|
|
610
|
+
withRedirect(redirect: RedirectMode): this;
|
|
611
|
+
/** The `Referer` to send (browser): a URL, `""` to send none, or `"about:client"` for the default. */
|
|
612
|
+
withReferrer(referrer: string): this;
|
|
613
|
+
/** How much referrer information the browser includes (`"no-referrer"`, `"strict-origin-when-cross-origin"`, …). */
|
|
614
|
+
withReferrerPolicy(policy: ReferrerPolicyName): this;
|
|
615
|
+
/** Priority hint for the browser's scheduler: `"high"`, `"low"` or `"auto"`. */
|
|
616
|
+
withPriority(priority: PriorityHint): this;
|
|
617
|
+
/** Lets the request outlive the page (analytics beacons, ≤ 64 KB body). */
|
|
618
|
+
withKeepAlive(keepalive?: boolean): this;
|
|
619
|
+
/** Subresource-integrity hash the response must match, e.g. `"sha256-…"`. */
|
|
620
|
+
withIntegrity(integrity: string): this;
|
|
621
|
+
/** How the browser's HTTP cache is used: `"default"`, `"no-store"`, `"reload"`, `"no-cache"`, `"force-cache"` or `"only-if-cached"`. */
|
|
622
|
+
withCache(cache: CacheMode): this;
|
|
623
|
+
/** Adds a {@link RequestInterceptor}. Api-level interceptors run first, then request-level ones, in registration order. */
|
|
624
|
+
withRequestInterceptor(interceptor: RequestInterceptor): this;
|
|
625
|
+
/** Adds a {@link ResponseInterceptor}, run after every successful response in registration order (it also receives the request). */
|
|
626
|
+
withResponseInterceptor(interceptor: ResponseInterceptor): this;
|
|
627
|
+
/** Adds an {@link ErrorInterceptor}, run once after the request has failed for good (after retries); it also receives the request, so it can replay it. */
|
|
628
|
+
withErrorInterceptor(interceptor: ErrorInterceptor): this;
|
|
629
|
+
/**
|
|
630
|
+
* Sets the request body (POST, PUT, PATCH and DELETE only — a compile error elsewhere). Objects and
|
|
631
|
+
* arrays are JSON-encoded; strings, `Blob`, `FormData`, `URLSearchParams`, `ArrayBuffer`, typed arrays
|
|
632
|
+
* and `ReadableStream` are sent as-is. `Content-Type` is set to `application/json` / `text/plain`
|
|
633
|
+
* unless already present, and removed for `FormData` (fetch must add the multipart boundary itself).
|
|
634
|
+
* Stream bodies are sent with `duplex: "half"` (Chromium and Node.js only) and are never retried.
|
|
635
|
+
*
|
|
636
|
+
* @example
|
|
637
|
+
* ```typescript
|
|
638
|
+
* create.post(url).withBody({ name: "Ada" });
|
|
639
|
+
* create.put(url).withBody(new FormData(form));
|
|
640
|
+
* ```
|
|
641
|
+
*/
|
|
642
|
+
withBody(this: HttpRequest<BodyMethod, T>, body: Body): this;
|
|
643
|
+
/**
|
|
644
|
+
* Sends a GraphQL operation as a JSON body (`{ query, variables }`). With `throwOnError`, a response
|
|
645
|
+
* whose `errors` array is non-empty rejects `getJson()` with a `RequestError` of code `"GRAPHQL"`.
|
|
646
|
+
*
|
|
647
|
+
* @example
|
|
648
|
+
* ```typescript
|
|
649
|
+
* const { user } = await create
|
|
650
|
+
* .post<{ data: { user: User } }>("/graphql")
|
|
651
|
+
* .withGraphQL("query ($id: ID!) { user(id: $id) { name } }", { id }, { throwOnError: true })
|
|
652
|
+
* .getData(r => r.data);
|
|
653
|
+
* ```
|
|
654
|
+
*/
|
|
655
|
+
withGraphQL(this: HttpRequest<BodyMethod, T>, query: string, variables?: object, options?: GraphQLOptions): this;
|
|
656
|
+
/**
|
|
657
|
+
* An independent copy of this request, so a configured request can serve as a template.
|
|
658
|
+
* Interceptors, signals and body objects are shared by reference (a `ReadableStream` body can only be sent once).
|
|
659
|
+
*
|
|
660
|
+
* @example
|
|
661
|
+
* ```typescript
|
|
662
|
+
* const search = api.get("/search").withTimeout(2000);
|
|
663
|
+
* const page1 = search.clone().withQueryParam("page", 1).getJson();
|
|
664
|
+
* const page2 = search.clone().withQueryParam("page", 2).getJson();
|
|
665
|
+
* ```
|
|
666
|
+
*/
|
|
667
|
+
clone(): HttpRequest<M, T>;
|
|
668
|
+
/**
|
|
669
|
+
* Sends the request (with retries, if configured) and resolves with the {@link ResponseWrapper} of a
|
|
670
|
+
* successful (2xx or opaque) response. Any failure — non-2xx status, network error, timeout, abort,
|
|
671
|
+
* interceptor error — rejects with a {@link RequestError}; use `getResult()` for a non-throwing variant.
|
|
672
|
+
*/
|
|
673
|
+
getResponse(): Promise<ResponseWrapper<T>>;
|
|
674
|
+
private _retriable;
|
|
675
|
+
private _attempt;
|
|
676
|
+
/** Attaches the CSRF token (see {@link withCsrf}) to `config.headers` when the final URL qualifies. */
|
|
677
|
+
private _csrf;
|
|
678
|
+
/**
|
|
679
|
+
* Sends the request and parses the body as JSON — see {@link ResponseWrapper.getJson} for the empty-body
|
|
680
|
+
* rule and schema validation.
|
|
681
|
+
*
|
|
682
|
+
* @example
|
|
683
|
+
* ```typescript
|
|
684
|
+
* const users = await api.get("/users").getJson<User[]>();
|
|
685
|
+
* const user = await api.get("/me").getJson(UserSchema); // validated + typed by the schema
|
|
686
|
+
* ```
|
|
687
|
+
*/
|
|
688
|
+
getJson<U = T>(): Promise<U>;
|
|
689
|
+
getJson<S extends StandardSchemaV1>(schema: S): Promise<StandardSchemaV1.InferOutput<S>>;
|
|
690
|
+
/** Sends the request and returns the body as text. */
|
|
691
|
+
getText(): Promise<string>;
|
|
692
|
+
/** Sends the request and returns the body as a `Blob` (downloads, images). */
|
|
693
|
+
getBlob(): Promise<Blob>;
|
|
694
|
+
/** Sends the request and returns the body as an `ArrayBuffer`. */
|
|
695
|
+
getArrayBuffer(): Promise<ArrayBuffer>;
|
|
696
|
+
/** Sends the request and parses the body as `FormData`. */
|
|
697
|
+
getFormData(): Promise<FormData>;
|
|
698
|
+
/** Sends the request and returns the raw body stream — see {@link ResponseWrapper.getBody}. */
|
|
699
|
+
getBody(): Promise<ReadableStream<Uint8Array> | null>;
|
|
700
|
+
/**
|
|
701
|
+
* Sends the request, parses JSON and applies a selector — see {@link ResponseWrapper.getData}.
|
|
702
|
+
*
|
|
703
|
+
* @example
|
|
704
|
+
* ```typescript
|
|
705
|
+
* const names = await api.get<Page<User>>("/users").getData(page => page.items.map(u => u.name));
|
|
706
|
+
* ```
|
|
707
|
+
*/
|
|
708
|
+
getData<U = T>(): Promise<U>;
|
|
709
|
+
getData<S extends StandardSchemaV1>(schema: S): Promise<StandardSchemaV1.InferOutput<S>>;
|
|
710
|
+
getData<S extends StandardSchemaV1, R>(schema: S, selector: (data: StandardSchemaV1.InferOutput<S>) => R): Promise<R>;
|
|
711
|
+
getData<R>(selector: (data: T) => R): Promise<R>;
|
|
712
|
+
getData<U, R>(selector: (data: U) => R): Promise<R>;
|
|
713
|
+
/**
|
|
714
|
+
* Sends the request and resolves with `{ data, error }` instead of throwing — for code that prefers
|
|
715
|
+
* errors as values. `error` is the same {@link RequestError} `getJson()` would have thrown.
|
|
716
|
+
*
|
|
717
|
+
* @example
|
|
718
|
+
* ```typescript
|
|
719
|
+
* const { data, error } = await api.get("/me").getResult<User>();
|
|
720
|
+
* if (error) return showError(error.message);
|
|
721
|
+
* render(data);
|
|
722
|
+
* ```
|
|
723
|
+
*/
|
|
724
|
+
getResult<U = T>(): Promise<RequestResult<U>>;
|
|
725
|
+
getResult<S extends StandardSchemaV1>(schema: S): Promise<RequestResult<StandardSchemaV1.InferOutput<S>>>;
|
|
726
|
+
}
|
|
727
|
+
//#endregion
|
|
728
|
+
//#region src/api.d.ts
|
|
729
|
+
/** Chainable `HttpRequest` methods that make sense as api-wide defaults (everything but body, signal and clone). */
|
|
730
|
+
type ApiKeys = Exclude<{ [K in keyof HttpRequest]: K extends `with${string}` | "onRetry" ? K : never; }[keyof HttpRequest], "withBody" | "withGraphQL" | "withSignal" | "withAbortController">;
|
|
731
|
+
type ApiChainables = { [K in keyof HttpRequest as K extends ApiKeys ? K : never]: HttpRequest[K] extends ((...args: infer A) => unknown) ? (...args: A) => ApiBuilder : never; };
|
|
732
|
+
/**
|
|
733
|
+
* A set of defaults for the requests it creates: base URL, headers, auth, timeout, retries, interceptors —
|
|
734
|
+
* every `with*` method of {@link HttpRequest} except the body and signal ones, with the same signatures.
|
|
735
|
+
*
|
|
736
|
+
* Api instances are **immutable**: each `with*` call returns a new instance and leaves the original
|
|
737
|
+
* untouched, so an api can be specialised safely (`const admin = api.withHeader("X-Role", "admin")`).
|
|
738
|
+
* Requests created from it can still override any default. Arguments are applied to every request the
|
|
739
|
+
* api creates (an object passed to `withHeaders` is read each time, so treat it as frozen).
|
|
740
|
+
*
|
|
741
|
+
* @example
|
|
742
|
+
* ```typescript
|
|
743
|
+
* export const api = createApi()
|
|
744
|
+
* .withBaseURL("https://api.example.com")
|
|
745
|
+
* .withBearerToken(token)
|
|
746
|
+
* .withTimeout(5000)
|
|
747
|
+
* .withRetries(2);
|
|
748
|
+
*
|
|
749
|
+
* const users = await api.get("/users").getJson<User[]>();
|
|
750
|
+
* const user = await api.post("/users").withBody({ name: "Ada" }).getJson<User>();
|
|
751
|
+
* ```
|
|
752
|
+
*/
|
|
753
|
+
interface ApiBuilder extends ApiChainables {
|
|
754
|
+
withHeaders<H extends { [K in keyof H]: string | number | null | undefined; }>(headers: H): ApiBuilder;
|
|
755
|
+
withCookies<C extends { [K in keyof C]: string; }>(cookies: C): ApiBuilder;
|
|
756
|
+
withQueryParams<P extends { [K in keyof P]: QueryValue; }>(params: P | URLSearchParams): ApiBuilder;
|
|
757
|
+
/**
|
|
758
|
+
* Prefixes relative paths with `baseURL`. Paths are *joined*, not resolved: `/v1` + `/users` → `/v1/users`,
|
|
759
|
+
* `./users` and `users` work the same, and absolute URLs (`https://…`, `//…`) are used as-is.
|
|
760
|
+
*/
|
|
761
|
+
withBaseURL(baseURL: string): ApiBuilder;
|
|
762
|
+
/** Creates a GET request. `T` declares the JSON type the response is expected to have. */
|
|
763
|
+
get<T = unknown>(path?: string): HttpRequest<"GET", T>;
|
|
764
|
+
/** Creates a HEAD request. */
|
|
765
|
+
head<T = unknown>(path?: string): HttpRequest<"HEAD", T>;
|
|
766
|
+
/** Creates an OPTIONS request. */
|
|
767
|
+
options<T = unknown>(path?: string): HttpRequest<"OPTIONS", T>;
|
|
768
|
+
/** Creates a POST request. */
|
|
769
|
+
post<T = unknown>(path?: string): HttpRequest<"POST", T>;
|
|
770
|
+
/** Creates a PUT request. */
|
|
771
|
+
put<T = unknown>(path?: string): HttpRequest<"PUT", T>;
|
|
772
|
+
/** Creates a PATCH request. */
|
|
773
|
+
patch<T = unknown>(path?: string): HttpRequest<"PATCH", T>;
|
|
774
|
+
/** Creates a DELETE request (`del` is an alias). */
|
|
775
|
+
delete<T = unknown>(path?: string): HttpRequest<"DELETE", T>;
|
|
776
|
+
/** Alias of `delete`. */
|
|
777
|
+
del<T = unknown>(path?: string): HttpRequest<"DELETE", T>;
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Creates an empty {@link ApiBuilder}. Configure it once, export it, and create every request through it.
|
|
781
|
+
* (`create.api()` is the same function.)
|
|
782
|
+
*/
|
|
783
|
+
export declare function createApi(): ApiBuilder;
|
|
784
|
+
//#endregion
|
|
785
|
+
//#region src/index.d.ts
|
|
786
|
+
/** A GET request. */
|
|
787
|
+
export type GetRequest<T = unknown> = HttpRequest<"GET", T>;
|
|
788
|
+
/** A HEAD request. */
|
|
789
|
+
export type HeadRequest<T = unknown> = HttpRequest<"HEAD", T>;
|
|
790
|
+
/** An OPTIONS request. */
|
|
791
|
+
export type OptionsRequest<T = unknown> = HttpRequest<"OPTIONS", T>;
|
|
792
|
+
/** A POST request. */
|
|
793
|
+
export type PostRequest<T = unknown> = HttpRequest<"POST", T>;
|
|
794
|
+
/** A PUT request. */
|
|
795
|
+
export type PutRequest<T = unknown> = HttpRequest<"PUT", T>;
|
|
796
|
+
/** A PATCH request. */
|
|
797
|
+
export type PatchRequest<T = unknown> = HttpRequest<"PATCH", T>;
|
|
798
|
+
/** A DELETE request. */
|
|
799
|
+
export type DeleteRequest<T = unknown> = HttpRequest<"DELETE", T>;
|
|
800
|
+
/** Any request — the v1 name for {@link HttpRequest}. */
|
|
801
|
+
export type BaseRequest<T = unknown> = HttpRequest<Method, T>;
|
|
802
|
+
/** Any request that may carry a body — the v1 name for `HttpRequest<"POST" | "PUT" | "PATCH" | "DELETE">`. */
|
|
803
|
+
export type BodyRequest<T = unknown> = HttpRequest<BodyMethod, T>;
|
|
804
|
+
/** Creates a GET request. `T` declares the JSON type the response is expected to have. */
|
|
805
|
+
export declare const createGet: <T = unknown>(url: string) => GetRequest<T>;
|
|
806
|
+
/** Creates a HEAD request. */
|
|
807
|
+
export declare const createHead: <T = unknown>(url: string) => HeadRequest<T>;
|
|
808
|
+
/** Creates an OPTIONS request. */
|
|
809
|
+
export declare const createOptions: <T = unknown>(url: string) => OptionsRequest<T>;
|
|
810
|
+
/** Creates a POST request. */
|
|
811
|
+
export declare const createPost: <T = unknown>(url: string) => PostRequest<T>;
|
|
812
|
+
/** Creates a PUT request. */
|
|
813
|
+
export declare const createPut: <T = unknown>(url: string) => PutRequest<T>;
|
|
814
|
+
/** Creates a PATCH request. */
|
|
815
|
+
export declare const createPatch: <T = unknown>(url: string) => PatchRequest<T>;
|
|
816
|
+
/** Creates a DELETE request. */
|
|
817
|
+
export declare const createDelete: <T = unknown>(url: string) => DeleteRequest<T>;
|
|
818
|
+
/**
|
|
819
|
+
* The entry point: one factory per HTTP method plus `api()` for configured instances.
|
|
820
|
+
*
|
|
821
|
+
* @example
|
|
822
|
+
* ```typescript
|
|
823
|
+
* import create from "create-request";
|
|
824
|
+
*
|
|
825
|
+
* const users = await create.get("https://api.example.com/users").getJson<User[]>();
|
|
826
|
+
* const api = create.api().withBaseURL("https://api.example.com").withBearerToken(token);
|
|
827
|
+
* const me = await api.get("/me").getJson<User>();
|
|
828
|
+
* ```
|
|
829
|
+
*/
|
|
830
|
+
declare const create: {
|
|
831
|
+
/** Creates a GET request. `T` declares the JSON type the response is expected to have. */
|
|
832
|
+
readonly get: <T = unknown>(url: string) => GetRequest<T>;
|
|
833
|
+
/** Creates a HEAD request. */
|
|
834
|
+
readonly head: <T = unknown>(url: string) => HeadRequest<T>;
|
|
835
|
+
/** Creates an OPTIONS request. */
|
|
836
|
+
readonly options: <T = unknown>(url: string) => OptionsRequest<T>;
|
|
837
|
+
/** Creates a POST request. */
|
|
838
|
+
readonly post: <T = unknown>(url: string) => PostRequest<T>;
|
|
839
|
+
/** Creates a PUT request. */
|
|
840
|
+
readonly put: <T = unknown>(url: string) => PutRequest<T>;
|
|
841
|
+
/** Creates a PATCH request. */
|
|
842
|
+
readonly patch: <T = unknown>(url: string) => PatchRequest<T>;
|
|
843
|
+
/** Creates a DELETE request. */
|
|
844
|
+
readonly delete: <T = unknown>(url: string) => DeleteRequest<T>;
|
|
845
|
+
/** Alias of `delete`. */
|
|
846
|
+
readonly del: <T = unknown>(url: string) => DeleteRequest<T>;
|
|
847
|
+
/** Creates an api instance — see {@link createApi}. */
|
|
848
|
+
readonly api: typeof createApi;
|
|
849
|
+
};
|
|
850
|
+
//#endregion
|
|
851
|
+
export { type ApiBuilder, type Body, type BodyMethod, type CookiesRecord, type CsrfOptions, type ErrorInterceptor, type FetchFunction, type GraphQLOptions, type HeadersRecord, type Method, type QueryParams, type QueryValue, type RequestConfig, type RequestErrorCode, type RequestErrorOptions, type RequestInterceptor, type RequestResult, type ResponseInterceptor, type RetryConfig, type RetryContext, type StandardSchemaV1, create as default };
|