create-request 2.1.0 → 2.2.1
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 +21 -0
- package/README.md +44 -12
- package/dist/index.cjs +25 -25
- package/dist/index.d.cts +67 -30
- package/dist/index.d.ts +67 -30
- package/dist/index.js +25 -26
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.1 — 2026-10-10
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- `getJson()`, `getText()`, `getBlob()`, `getArrayBuffer()`, `getFormData()`, `getData()` and `getResult()` now read the body as part of each attempt, so a failure while reading it goes through the retry policy and the error interceptors like a failure before the response. Before, the body was read after both had finished: a body that stalled past `withTimeout()` was not retried, and error interceptors never saw a timeout or abort during the read, a connection dropped mid-body, invalid JSON, a schema mismatch or GraphQL errors. The default policy retries only the timeout; a custom `shouldRetry` now also receives the other errors (with `status` set). A `ResponseWrapper` returned by an error interceptor is read by the same reader, and if that read fails, the next interceptor receives the error. `getResponse()` and `getBody()` are unchanged: they resolve before the body is read.
|
|
8
|
+
- A `getData()` selector that returns a rejected promise is reported as a `"PARSE"` error, like a selector that throws; it used to reject with the raw error.
|
|
9
|
+
- A Node.js stream (`fs.createReadStream()`, a `Readable`) or another async iterable passed to `withBody()` is sent as a stream body (`duplex: "half"`, never retried), the way undici's `fetch` expects. Before, it was JSON-encoded: `withBody(fs.createReadStream(file))` sent the stream object's fields, local file path included, as `application/json`, and an async generator sent `{}`.
|
|
10
|
+
- `getBody()` now has the type `fetch` gives `response.body` (`Response["body"]`), so piping it through a `TextDecoderStream`, the usual way to read server-sent events or LLM output, compiles with TypeScript 5.9 and later. Its old type, `ReadableStream<Uint8Array>`, allows `SharedArrayBuffer`-backed chunks, which `TextDecoderStream` in the TypeScript 5.9 DOM lib rejects. In a Node-only project (`@types/node` without the DOM lib) the type is `@types/node`'s, as for `fetch`.
|
|
11
|
+
|
|
12
|
+
### Internal
|
|
13
|
+
|
|
14
|
+
- The full import is 16 B smaller (min+gzip) than in 2.2.0 despite both fixes: class fields are declared instead of being emitted as parameter properties, and the schema check is shorter.
|
|
15
|
+
- CI fails on packaging problems found by attw or publint (`npm run pack:check`); the build only warned about them.
|
|
16
|
+
- The release workflow runs the full check and packs the tarball in a job without credentials. A separate job in the `npm` environment publishes that tarball with trusted publishing and provenance; it checks out nothing and runs no scripts, so no dependency runs while an npm token can be minted. A third job creates the GitHub release. Actions are pinned to commit SHAs and kept current by Dependabot.
|
|
17
|
+
|
|
18
|
+
## 2.2.0 — 2026-10-09
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- `create.query()`, `createQuery()`, `api.query()` and the `QueryRequest` type for the HTTP `QUERY` method ([RFC 10008](https://www.rfc-editor.org/rfc/rfc10008)): a safe, idempotent read whose query is sent in the body, for searches too large or too structured for a URL. `"QUERY"` joins the `Method` and `BodyMethod` unions, so `withBody()` and `withGraphQL()` accept it and `withRetries({ methods })` can list it; a `switch` that handles every `Method` needs a `"QUERY"` case.
|
|
23
|
+
|
|
3
24
|
## 2.1.0 — 2026-10-03
|
|
4
25
|
|
|
5
26
|
### Added
|
package/README.md
CHANGED
|
@@ -104,15 +104,26 @@ try {
|
|
|
104
104
|
### Creating
|
|
105
105
|
|
|
106
106
|
```typescript
|
|
107
|
-
create.get(url); // also head, options, post, put, patch, delete (alias: del)
|
|
107
|
+
create.get(url); // also head, options, post, put, patch, delete (alias: del), query
|
|
108
108
|
api.get("/users"); // joined to the api's base URL
|
|
109
109
|
api.get(); // no path → the base URL itself
|
|
110
110
|
api.get<User>("/me"); // declare the JSON type once; getJson() / getData() / getResult() use it
|
|
111
111
|
```
|
|
112
112
|
|
|
113
113
|
Named factories exist as well: `createGet`, `createPost`, `createPut`, `createPatch`,
|
|
114
|
-
`createDelete`, `createHead`, `createOptions` (the same functions as
|
|
115
|
-
`createApi()` is also available as `create.api()`.
|
|
114
|
+
`createDelete`, `createHead`, `createOptions`, `createQuery` (the same functions as
|
|
115
|
+
`create.get`, …), and `createApi()` is also available as `create.api()`.
|
|
116
|
+
|
|
117
|
+
`QUERY` works like `GET`, but the query goes in the body:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
const results = await api
|
|
121
|
+
.query<Page<Post>>("/posts/search")
|
|
122
|
+
.withBody({ text: "fetch", tags: ["http"], sort: "recent" })
|
|
123
|
+
.getJson();
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The server must support `QUERY`. In browsers, a cross-origin `QUERY` request is preflighted.
|
|
116
127
|
|
|
117
128
|
### Configuring
|
|
118
129
|
|
|
@@ -129,8 +140,8 @@ create
|
|
|
129
140
|
// and a key set twice keeps the last value (a request can override an api default)
|
|
130
141
|
.withQueryParams({ page: 2, tags: ["a", "b"], since: new Date() })
|
|
131
142
|
.withQueryParam("q", "search term")
|
|
132
|
-
// body (POST, PUT, PATCH, DELETE only): objects → JSON, strings → text/plain,
|
|
133
|
-
// FormData / Blob / URLSearchParams / ArrayBuffer / typed arrays /
|
|
143
|
+
// body (POST, PUT, PATCH, DELETE, QUERY only): objects → JSON, strings → text/plain,
|
|
144
|
+
// FormData / Blob / URLSearchParams / ArrayBuffer / typed arrays / streams → sent as-is
|
|
134
145
|
.withBody({ name: "Ada" })
|
|
135
146
|
// resilience
|
|
136
147
|
.withTimeout(5000) // per attempt; covers the response and the body read
|
|
@@ -296,7 +307,7 @@ api.get("/status").withRetries({
|
|
|
296
307
|
attempts: 3,
|
|
297
308
|
delay: ({ attempt }) => attempt * 500, // ms, or a number; default: exponential backoff
|
|
298
309
|
statuses: [503], // default: 408, 425, 429, 500, 502, 503, 504
|
|
299
|
-
methods: ["GET", "HEAD", "OPTIONS", "PUT", "DELETE"], // default: all, POST and PATCH included
|
|
310
|
+
methods: ["GET", "HEAD", "OPTIONS", "QUERY", "PUT", "DELETE"], // default: all, POST and PATCH included
|
|
300
311
|
maxDelay: 10_000, // caps the backoff (default 30 s); a longer Retry-After gives up instead
|
|
301
312
|
shouldRetry: ({ error }) => error.code === "NETWORK", // full override of the decision
|
|
302
313
|
onRetry: ({ attempt, error, delay }) =>
|
|
@@ -310,14 +321,17 @@ How retries behave:
|
|
|
310
321
|
backoff (300 ms, 600 ms, 1.2 s, … plus up to 100 ms of jitter, capped at `maxDelay`);
|
|
311
322
|
validation errors and other failures are not.
|
|
312
323
|
- Every method is retried by default, `POST` and `PATCH` included. Pass
|
|
313
|
-
`methods: ["GET", "HEAD", "OPTIONS", "PUT", "DELETE"]` if a repeated request could
|
|
314
|
-
work.
|
|
324
|
+
`methods: ["GET", "HEAD", "OPTIONS", "QUERY", "PUT", "DELETE"]` if a repeated request could
|
|
325
|
+
duplicate work.
|
|
315
326
|
- A `Retry-After` header is honoured unless you set `delay`; one longer than `maxDelay` cancels
|
|
316
327
|
the retry so you can react yourself.
|
|
317
328
|
- Aborted requests and requests with a stream body are never retried, whatever the policy. That
|
|
318
329
|
includes a timeout from your own `AbortSignal.timeout()` signal, which stays aborted (only
|
|
319
330
|
`withTimeout()` timeouts are retried).
|
|
320
|
-
- The timeout applies to each attempt
|
|
331
|
+
- The timeout applies to each attempt. With `getJson()`, `getText()`, `getData()` and the other
|
|
332
|
+
readers an attempt includes reading the body, so a body that stalls past the timeout is
|
|
333
|
+
retried too; `getResponse()` and `getBody()` hand you the body unread.
|
|
334
|
+
- Error interceptors run once, after the last attempt.
|
|
321
335
|
|
|
322
336
|
`onRetry(callback)` also exists as a method; it does not enable retries by itself.
|
|
323
337
|
|
|
@@ -381,6 +395,11 @@ it. A request or response interceptor that throws fails the request with code `"
|
|
|
381
395
|
an error interceptor that throws replaces the error with an `"INTERCEPTOR"` one (a thrown
|
|
382
396
|
`RequestError` is kept as-is).
|
|
383
397
|
|
|
398
|
+
Error interceptors also see failures while `getJson()`, `getText()` and the other readers read
|
|
399
|
+
the body: invalid JSON (`"PARSE"`), a schema mismatch (`"VALIDATION"`), GraphQL errors
|
|
400
|
+
(`"GRAPHQL"`) and a timeout or abort during the read. A `ResponseWrapper` they return is read the
|
|
401
|
+
same way, so `getJson()` parses the recovered response.
|
|
402
|
+
|
|
384
403
|
## Schema validation
|
|
385
404
|
|
|
386
405
|
Pass any [Standard Schema](https://standardschema.dev) (zod 3.24+, valibot 1+, arktype 2+,
|
|
@@ -434,8 +453,21 @@ for (;;) {
|
|
|
434
453
|
}
|
|
435
454
|
```
|
|
436
455
|
|
|
456
|
+
For text streams (server-sent events, NDJSON, LLM output), pipe the body through a
|
|
457
|
+
`TextDecoderStream`:
|
|
458
|
+
|
|
459
|
+
```typescript
|
|
460
|
+
const stream = await api.post("/chat").withBody({ prompt: "Hello" }).getBody();
|
|
461
|
+
const reader = stream!.pipeThrough(new TextDecoderStream()).getReader();
|
|
462
|
+
for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
|
|
463
|
+
render(chunk.value);
|
|
464
|
+
}
|
|
465
|
+
```
|
|
466
|
+
|
|
437
467
|
Request bodies can be streams too (`withBody(readableStream)`), sent with `duplex: "half"`.
|
|
438
|
-
Node.js and Chromium support that; Firefox and Safari do not.
|
|
468
|
+
Node.js and Chromium support that; Firefox and Safari do not. In Node.js a Node stream or any
|
|
469
|
+
async iterable works the same way, so `withBody(fs.createReadStream(path))` uploads a file (set
|
|
470
|
+
its `Content-Type` yourself). A stream body can be sent only once and is never retried.
|
|
439
471
|
Upload _progress_ is not something `fetch` exposes in browsers, so there is no API for it.
|
|
440
472
|
|
|
441
473
|
## Testing and custom fetch
|
|
@@ -531,8 +563,8 @@ Measured with `size-limit` on the published build of this version (`npm run size
|
|
|
531
563
|
|
|
532
564
|
| Import | min + gzip | min + brotli |
|
|
533
565
|
| ------------------------------ | ---------: | -----------: |
|
|
534
|
-
| everything (`import * as …`) | 4.
|
|
535
|
-
| `import { createGet }` only | 4.
|
|
566
|
+
| everything (`import * as …`) | 4.93 KB | 4.47 KB |
|
|
567
|
+
| `import { createGet }` only | 4.30 KB | |
|
|
536
568
|
| `import { RequestError }` only | 0.25 KB | |
|
|
537
569
|
|
|
538
570
|
The package is one module with no side effects, so bundlers drop whatever you do not import. The
|
package/dist/index.cjs
CHANGED
|
@@ -34,7 +34,7 @@ const messageOf = (error) => error instanceof Error ? error.message : String(err
|
|
|
34
34
|
|
|
35
35
|
//#endregion
|
|
36
36
|
//#region src/schema.ts
|
|
37
|
-
const isSchema = (value) =>
|
|
37
|
+
const isSchema = (value) => "~standard" in Object(value);
|
|
38
38
|
|
|
39
39
|
//#endregion
|
|
40
40
|
//#region src/utils.ts
|
|
@@ -124,9 +124,6 @@ const retryAfter = (response) => {
|
|
|
124
124
|
//#endregion
|
|
125
125
|
//#region src/response.ts
|
|
126
126
|
var ResponseWrapper = class {
|
|
127
|
-
raw;
|
|
128
|
-
url;
|
|
129
|
-
method;
|
|
130
127
|
constructor(raw, url = "", method = "GET") {
|
|
131
128
|
this.raw = raw;
|
|
132
129
|
this.url = url;
|
|
@@ -240,7 +237,7 @@ var ResponseWrapper = class {
|
|
|
240
237
|
const data = await this.getJson(schema);
|
|
241
238
|
if (!select) return data;
|
|
242
239
|
try {
|
|
243
|
-
return select(data);
|
|
240
|
+
return await select(data);
|
|
244
241
|
} catch (e) {
|
|
245
242
|
throw this._err(`Selector failed: ${messageOf(e)}`, "PARSE", { cause: e });
|
|
246
243
|
}
|
|
@@ -259,7 +256,6 @@ const RETRY_STATUSES = [
|
|
|
259
256
|
504
|
|
260
257
|
];
|
|
261
258
|
var HttpRequest = class HttpRequest {
|
|
262
|
-
method;
|
|
263
259
|
_o = {
|
|
264
260
|
init: {},
|
|
265
261
|
headers: {},
|
|
@@ -269,7 +265,6 @@ var HttpRequest = class HttpRequest {
|
|
|
269
265
|
res: [],
|
|
270
266
|
err: []
|
|
271
267
|
};
|
|
272
|
-
_url;
|
|
273
268
|
constructor(method, url) {
|
|
274
269
|
this.method = method;
|
|
275
270
|
this._url = url;
|
|
@@ -417,8 +412,8 @@ var HttpRequest = class HttpRequest {
|
|
|
417
412
|
withBody(body) {
|
|
418
413
|
const o = this._o;
|
|
419
414
|
const tag = Object.prototype.toString.call(body).slice(8, -1);
|
|
420
|
-
|
|
421
|
-
o.stream
|
|
415
|
+
o.stream = Symbol.asyncIterator in Object(body) || tag === "ReadableStream";
|
|
416
|
+
const raw = o.stream || ArrayBuffer.isView(body) || /^(String|Blob|File|FormData|URLSearchParams|ArrayBuffer)$/.test(tag);
|
|
422
417
|
if (raw) o.body = body;
|
|
423
418
|
else try {
|
|
424
419
|
o.body = JSON.stringify(body);
|
|
@@ -451,12 +446,15 @@ var HttpRequest = class HttpRequest {
|
|
|
451
446
|
};
|
|
452
447
|
return copy;
|
|
453
448
|
}
|
|
454
|
-
|
|
449
|
+
getResponse() {
|
|
450
|
+
return this._run((response) => response);
|
|
451
|
+
}
|
|
452
|
+
async _run(read) {
|
|
455
453
|
const o = this._o;
|
|
456
454
|
const retry = o.retry;
|
|
457
455
|
let error;
|
|
458
456
|
for (let attempt = 0;; attempt++) try {
|
|
459
|
-
return await this._attempt();
|
|
457
|
+
return await read(await this._attempt());
|
|
460
458
|
} catch (e) {
|
|
461
459
|
error = e instanceof RequestError ? e : this._fail(`Unexpected error: ${messageOf(e)}`, "NETWORK", { cause: e });
|
|
462
460
|
const context = {
|
|
@@ -486,7 +484,7 @@ var HttpRequest = class HttpRequest {
|
|
|
486
484
|
}
|
|
487
485
|
for (const interceptor of o.err) try {
|
|
488
486
|
const result = await interceptor(error, this);
|
|
489
|
-
if (result instanceof ResponseWrapper) return result;
|
|
487
|
+
if (result instanceof ResponseWrapper) return await read(result);
|
|
490
488
|
if (result) error = result;
|
|
491
489
|
} catch (e) {
|
|
492
490
|
error = e instanceof RequestError ? e : this._fail(`Error interceptor failed: ${messageOf(e)}`, "INTERCEPTOR", {
|
|
@@ -618,25 +616,25 @@ var HttpRequest = class HttpRequest {
|
|
|
618
616
|
if (token && !(header in config.headers)) config.headers[header] = token;
|
|
619
617
|
}
|
|
620
618
|
getJson(schema) {
|
|
621
|
-
return this.
|
|
619
|
+
return this._run((response) => response.getJson(schema));
|
|
622
620
|
}
|
|
623
621
|
getText() {
|
|
624
|
-
return this.
|
|
622
|
+
return this._run((response) => response.getText());
|
|
625
623
|
}
|
|
626
624
|
getBlob() {
|
|
627
|
-
return this.
|
|
625
|
+
return this._run((response) => response.getBlob());
|
|
628
626
|
}
|
|
629
627
|
getArrayBuffer() {
|
|
630
|
-
return this.
|
|
628
|
+
return this._run((response) => response.getArrayBuffer());
|
|
631
629
|
}
|
|
632
630
|
getFormData() {
|
|
633
|
-
return this.
|
|
631
|
+
return this._run((response) => response.getFormData());
|
|
634
632
|
}
|
|
635
633
|
getBody() {
|
|
636
|
-
return this.
|
|
634
|
+
return this._run((response) => response.getBody());
|
|
637
635
|
}
|
|
638
636
|
getData(schemaOrSelector, selector) {
|
|
639
|
-
return this.
|
|
637
|
+
return this._run((response) => response.getData(schemaOrSelector, selector));
|
|
640
638
|
}
|
|
641
639
|
async getResult(schema) {
|
|
642
640
|
try {
|
|
@@ -657,11 +655,9 @@ var HttpRequest = class HttpRequest {
|
|
|
657
655
|
//#region src/api.ts
|
|
658
656
|
const ABSOLUTE = /^([a-z][a-z0-9+.-]*:)?\/\//i;
|
|
659
657
|
var Api = class Api {
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
this._base = _base;
|
|
664
|
-
this._defaults = _defaults;
|
|
658
|
+
constructor(base = "", defaults = []) {
|
|
659
|
+
this._base = base;
|
|
660
|
+
this._defaults = defaults;
|
|
665
661
|
}
|
|
666
662
|
_with(fn) {
|
|
667
663
|
fn(new HttpRequest("GET", ""));
|
|
@@ -685,7 +681,8 @@ const METHODS = [
|
|
|
685
681
|
"POST",
|
|
686
682
|
"PUT",
|
|
687
683
|
"PATCH",
|
|
688
|
-
"DELETE"
|
|
684
|
+
"DELETE",
|
|
685
|
+
"QUERY"
|
|
689
686
|
];
|
|
690
687
|
let installed = false;
|
|
691
688
|
function createApi() {
|
|
@@ -712,6 +709,7 @@ const createPost = (url) => new HttpRequest("POST", url);
|
|
|
712
709
|
const createPut = (url) => new HttpRequest("PUT", url);
|
|
713
710
|
const createPatch = (url) => new HttpRequest("PATCH", url);
|
|
714
711
|
const createDelete = (url) => new HttpRequest("DELETE", url);
|
|
712
|
+
const createQuery = (url) => new HttpRequest("QUERY", url);
|
|
715
713
|
const create = {
|
|
716
714
|
get: createGet,
|
|
717
715
|
head: createHead,
|
|
@@ -721,6 +719,7 @@ const create = {
|
|
|
721
719
|
patch: createPatch,
|
|
722
720
|
delete: createDelete,
|
|
723
721
|
del: createDelete,
|
|
722
|
+
query: createQuery,
|
|
724
723
|
api: createApi
|
|
725
724
|
};
|
|
726
725
|
|
|
@@ -736,5 +735,6 @@ exports.createOptions = createOptions;
|
|
|
736
735
|
exports.createPatch = createPatch;
|
|
737
736
|
exports.createPost = createPost;
|
|
738
737
|
exports.createPut = createPut;
|
|
738
|
+
exports.createQuery = createQuery;
|
|
739
739
|
exports.default = create;
|
|
740
740
|
exports.isRequestError = isRequestError;
|
package/dist/index.d.cts
CHANGED
|
@@ -163,10 +163,13 @@ export declare class RequestError<TData = unknown> extends Error {
|
|
|
163
163
|
export declare const isRequestError: (error: unknown) => error is RequestError;
|
|
164
164
|
//#endregion
|
|
165
165
|
//#region src/types.d.ts
|
|
166
|
-
/**
|
|
167
|
-
|
|
166
|
+
/**
|
|
167
|
+
* The HTTP methods a request can be created with. `QUERY` ([RFC 10008](https://www.rfc-editor.org/rfc/rfc10008)) is a
|
|
168
|
+
* safe, idempotent read like `GET` whose query is sent in the body.
|
|
169
|
+
*/
|
|
170
|
+
type Method = "GET" | "HEAD" | "OPTIONS" | "DELETE" | "POST" | "PUT" | "PATCH" | "QUERY";
|
|
168
171
|
/** The HTTP methods that may carry a request body (`withBody` / `withGraphQL` are only available on these). */
|
|
169
|
-
type BodyMethod = "POST" | "PUT" | "PATCH" | "DELETE";
|
|
172
|
+
type BodyMethod = "POST" | "PUT" | "PATCH" | "DELETE" | "QUERY";
|
|
170
173
|
/** What `fetch` accepts as a body. */
|
|
171
174
|
type FetchBody = NonNullable<RequestInit["body"]>;
|
|
172
175
|
/** `"include"`, `"omit"` or `"same-origin"`. */
|
|
@@ -186,7 +189,8 @@ type PriorityHint = "auto" | "high" | "low";
|
|
|
186
189
|
*
|
|
187
190
|
* - `string` → sent as-is (`Content-Type: text/plain` unless you set one)
|
|
188
191
|
* - `Blob` / `File`, `FormData`, `URLSearchParams` → sent as-is, `fetch` sets the matching `Content-Type`
|
|
189
|
-
* - `ArrayBuffer`, typed arrays, `ReadableStream`
|
|
192
|
+
* - `ArrayBuffer`, typed arrays, `ReadableStream` and, in Node.js, Node streams and other async iterables → sent as-is,
|
|
193
|
+
* no `Content-Type` unless you set one
|
|
190
194
|
* - any other JSON-serialisable object or array → `JSON.stringify`-ed (`Content-Type: application/json` unless you set one)
|
|
191
195
|
*
|
|
192
196
|
* @example
|
|
@@ -237,6 +241,8 @@ interface RetryContext {
|
|
|
237
241
|
* 408, 425, 429, 500, 502, 503 or 504, with exponential backoff (300 ms, 600 ms, 1.2 s, … plus
|
|
238
242
|
* up to 100 ms of jitter, capped at `maxDelay`). A `Retry-After` header is honoured when present.
|
|
239
243
|
* Aborted requests and requests with a `ReadableStream` body are never retried, whatever the policy.
|
|
244
|
+
* With `getJson()`, `getText()` and the other readers an attempt includes reading the body, so a timeout
|
|
245
|
+
* during the read is retried too (`getResponse()` and `getBody()` hand the body over unread).
|
|
240
246
|
*
|
|
241
247
|
* @example
|
|
242
248
|
* ```typescript
|
|
@@ -261,14 +267,17 @@ interface RetryConfig {
|
|
|
261
267
|
delay?: number | ((context: RetryContext) => number) | undefined;
|
|
262
268
|
/** Statuses that are retried. Default: `[408, 425, 429, 500, 502, 503, 504]`. Network errors and timeouts are retried by default. */
|
|
263
269
|
statuses?: readonly number[] | undefined;
|
|
264
|
-
/** Methods that are retried. Default: all. Use `["GET", "HEAD", "OPTIONS", "PUT", "DELETE"]` to retry idempotent requests only. */
|
|
270
|
+
/** Methods that are retried. Default: all. Use `["GET", "HEAD", "OPTIONS", "QUERY", "PUT", "DELETE"]` to retry idempotent requests only. */
|
|
265
271
|
methods?: readonly Method[] | undefined;
|
|
266
272
|
/**
|
|
267
273
|
* Upper bound, in milliseconds, for the default backoff. A `Retry-After` header longer than this
|
|
268
274
|
* cancels the retry so you can react to it yourself. Default: `30000`.
|
|
269
275
|
*/
|
|
270
276
|
maxDelay?: number | undefined;
|
|
271
|
-
/**
|
|
277
|
+
/**
|
|
278
|
+
* Full override of the retry decision (`statuses` and `methods` are ignored). Aborts and stream bodies still never retry.
|
|
279
|
+
* It also receives the errors raised while a reader reads the body (`PARSE`, `VALIDATION`, `GRAPHQL`, with `status` set).
|
|
280
|
+
*/
|
|
272
281
|
shouldRetry?: ((context: RetryContext) => boolean | Promise<boolean>) | undefined;
|
|
273
282
|
/** Called before every retry, after the delay has been computed. Awaited if it returns a promise. */
|
|
274
283
|
onRetry?: ((context: RetryContext & {
|
|
@@ -327,6 +336,10 @@ type ResponseInterceptor = (response: ResponseWrapper, request: HttpRequest) =>
|
|
|
327
336
|
* Return nothing to keep the error, another {@link RequestError} to replace it, or a
|
|
328
337
|
* {@link ResponseWrapper} to recover — typically by replaying `request.clone()`. Throwing replaces the error as well.
|
|
329
338
|
*
|
|
339
|
+
* With `getJson()`, `getText()` and the other readers, a failure while reading the body (`PARSE`, `VALIDATION`,
|
|
340
|
+
* `GRAPHQL`, or a timeout or abort during the read) also reaches error interceptors, and a recovered
|
|
341
|
+
* `ResponseWrapper` is read the same way; if that read fails, the next interceptor receives the new error.
|
|
342
|
+
*
|
|
330
343
|
* @example
|
|
331
344
|
* ```typescript
|
|
332
345
|
* const replayed = new WeakSet<HttpRequest>();
|
|
@@ -388,23 +401,17 @@ type RequestResult<T> = {
|
|
|
388
401
|
* ```
|
|
389
402
|
*/
|
|
390
403
|
export declare class ResponseWrapper<T = unknown> {
|
|
404
|
+
private _bin?;
|
|
405
|
+
private _text?;
|
|
406
|
+
private _json?;
|
|
391
407
|
/** The underlying `Response`. Read its body through this wrapper's methods, or take it over with `getBody()`. */
|
|
392
408
|
readonly raw: Response;
|
|
393
409
|
/** The URL that was requested, including the query string (after interceptors). */
|
|
394
410
|
readonly url: string;
|
|
395
411
|
/** The HTTP method that was used. */
|
|
396
412
|
readonly method: Method;
|
|
397
|
-
private _bin?;
|
|
398
|
-
private _text?;
|
|
399
|
-
private _json?;
|
|
400
413
|
/** Wraps a `Response` — useful to hand a synthetic response to an error interceptor. */
|
|
401
|
-
constructor(
|
|
402
|
-
/** The underlying `Response`. Read its body through this wrapper's methods, or take it over with `getBody()`. */
|
|
403
|
-
raw: Response,
|
|
404
|
-
/** The URL that was requested, including the query string (after interceptors). */
|
|
405
|
-
url?: string,
|
|
406
|
-
/** The HTTP method that was used. */
|
|
407
|
-
method?: Method);
|
|
414
|
+
constructor(raw: Response, url?: string, method?: Method);
|
|
408
415
|
/** HTTP status code (`200`, `404`, …). */
|
|
409
416
|
get status(): number;
|
|
410
417
|
/** HTTP status text (`"OK"`, `"Not Found"`, …); empty over HTTP/2. */
|
|
@@ -430,6 +437,8 @@ export declare class ResponseWrapper<T = unknown> {
|
|
|
430
437
|
* The raw body stream, for streaming consumption (downloads with progress, SSE/NDJSON, LLM output…).
|
|
431
438
|
* Unlike the other readers it is not buffered: the body can be read once, and only if no other reader ran.
|
|
432
439
|
* Taking the stream also ends the request's `withTimeout()` deadline — from here on the stream is yours.
|
|
440
|
+
* It has the type `fetch` gives `response.body` in your environment (DOM lib or `@types/node`), so it pipes
|
|
441
|
+
* through `TextDecoderStream` and other web streams like that one does.
|
|
433
442
|
*
|
|
434
443
|
* @example
|
|
435
444
|
* ```typescript
|
|
@@ -437,7 +446,7 @@ export declare class ResponseWrapper<T = unknown> {
|
|
|
437
446
|
* for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) process(chunk.value);
|
|
438
447
|
* ```
|
|
439
448
|
*/
|
|
440
|
-
getBody():
|
|
449
|
+
getBody(): Response["body"];
|
|
441
450
|
/**
|
|
442
451
|
* The body parsed as JSON. An empty body (`204`, `Content-Length: 0`, whitespace) yields `null` —
|
|
443
452
|
* declare it in the type (`getJson<User | null>()`) for endpoints that may return nothing.
|
|
@@ -457,7 +466,8 @@ export declare class ResponseWrapper<T = unknown> {
|
|
|
457
466
|
/**
|
|
458
467
|
* `getJson()` followed by a selector, so you can pick the part you need in one call. The selector
|
|
459
468
|
* receives `T` (declare it on the request: `api.get<Page>()`), or pass both type arguments explicitly.
|
|
460
|
-
*
|
|
469
|
+
* A selector that throws (or returns a promise that rejects) is reported as a `RequestError` with code `"PARSE"`.
|
|
470
|
+
* A schema can be validated first.
|
|
461
471
|
*
|
|
462
472
|
* @example
|
|
463
473
|
* ```typescript
|
|
@@ -501,9 +511,7 @@ export declare class HttpRequest<M extends Method = Method, T = unknown> {
|
|
|
501
511
|
readonly method: M;
|
|
502
512
|
private readonly _url;
|
|
503
513
|
/** Prefer `create.get(url)`, `create.post(url)`, … or an api instance to construct requests. */
|
|
504
|
-
constructor(
|
|
505
|
-
/** The HTTP method. */
|
|
506
|
-
method: M, url: string);
|
|
514
|
+
constructor(method: M, url: string);
|
|
507
515
|
/** The URL this request will be sent to, including the query string. */
|
|
508
516
|
get url(): string;
|
|
509
517
|
private _fail;
|
|
@@ -648,14 +656,18 @@ export declare class HttpRequest<M extends Method = Method, T = unknown> {
|
|
|
648
656
|
withRequestInterceptor(interceptor: RequestInterceptor): this;
|
|
649
657
|
/** Adds a {@link ResponseInterceptor}, run after every successful response in registration order (it also receives the request). */
|
|
650
658
|
withResponseInterceptor(interceptor: ResponseInterceptor): this;
|
|
651
|
-
/**
|
|
659
|
+
/**
|
|
660
|
+
* Adds an {@link ErrorInterceptor}, run once after the request has failed for good (after retries, and including a failure while
|
|
661
|
+
* `getJson()`, `getText()`, … read the body); it also receives the request, so it can replay it.
|
|
662
|
+
*/
|
|
652
663
|
withErrorInterceptor(interceptor: ErrorInterceptor): this;
|
|
653
664
|
/**
|
|
654
|
-
* Sets the request body (POST, PUT, PATCH and
|
|
655
|
-
* arrays are JSON-encoded; strings, `Blob`, `FormData`, `URLSearchParams`, `ArrayBuffer`, typed arrays
|
|
656
|
-
*
|
|
657
|
-
*
|
|
658
|
-
* Stream bodies are sent with `duplex: "half"`
|
|
665
|
+
* Sets the request body (POST, PUT, PATCH, DELETE and QUERY only — a compile error elsewhere). Objects and
|
|
666
|
+
* arrays are JSON-encoded; strings, `Blob`, `FormData`, `URLSearchParams`, `ArrayBuffer`, typed arrays,
|
|
667
|
+
* `ReadableStream` and, in Node.js, Node streams (`fs.createReadStream()`) and other async iterables are sent
|
|
668
|
+
* as-is. `Content-Type` is set to `application/json` / `text/plain` unless already present, and removed for
|
|
669
|
+
* `FormData` (fetch must add the multipart boundary itself). Stream bodies are sent with `duplex: "half"`
|
|
670
|
+
* (Chromium and Node.js only), can be sent only once and are never retried.
|
|
659
671
|
*
|
|
660
672
|
* @example
|
|
661
673
|
* ```typescript
|
|
@@ -679,7 +691,7 @@ export declare class HttpRequest<M extends Method = Method, T = unknown> {
|
|
|
679
691
|
withGraphQL(this: HttpRequest<BodyMethod, T>, query: string, variables?: object, options?: GraphQLOptions): this;
|
|
680
692
|
/**
|
|
681
693
|
* An independent copy of this request, so a configured request can serve as a template.
|
|
682
|
-
* Interceptors, signals and body objects are shared by reference (a
|
|
694
|
+
* Interceptors, signals and body objects are shared by reference (a stream body can only be sent once).
|
|
683
695
|
*
|
|
684
696
|
* @example
|
|
685
697
|
* ```typescript
|
|
@@ -693,8 +705,16 @@ export declare class HttpRequest<M extends Method = Method, T = unknown> {
|
|
|
693
705
|
* Sends the request (with retries, if configured) and resolves with the {@link ResponseWrapper} of a
|
|
694
706
|
* successful (2xx or opaque) response. Any failure — non-2xx status, network error, timeout, abort,
|
|
695
707
|
* interceptor error — rejects with a {@link RequestError}; use `getResult()` for a non-throwing variant.
|
|
708
|
+
* It resolves before the body is read, so reading it is up to you and happens after the retries and error
|
|
709
|
+
* interceptors; `getJson()`, `getText()` and the other readers read it as part of each attempt instead.
|
|
696
710
|
*/
|
|
697
711
|
getResponse(): Promise<ResponseWrapper<T>>;
|
|
712
|
+
/**
|
|
713
|
+
* Runs the attempts, the retry policy and the error interceptors around `read`, which receives each response — so a
|
|
714
|
+
* failure while reading the body (a timeout during the read, invalid JSON, a schema or GraphQL error) is retried and
|
|
715
|
+
* intercepted like a failure before it. A response recovered by an error interceptor goes through `read` as well.
|
|
716
|
+
*/
|
|
717
|
+
private _run;
|
|
698
718
|
private _retriable;
|
|
699
719
|
private _attempt;
|
|
700
720
|
/** Attaches the CSRF token (see {@link withCsrf}) to `config.headers` when the final URL qualifies. */
|
|
@@ -720,7 +740,7 @@ export declare class HttpRequest<M extends Method = Method, T = unknown> {
|
|
|
720
740
|
/** Sends the request and parses the body as `FormData`. */
|
|
721
741
|
getFormData(): Promise<FormData>;
|
|
722
742
|
/** Sends the request and returns the raw body stream — see {@link ResponseWrapper.getBody}. */
|
|
723
|
-
getBody(): Promise<
|
|
743
|
+
getBody(): Promise<Response["body"]>;
|
|
724
744
|
/**
|
|
725
745
|
* Sends the request, parses JSON and applies a selector — see {@link ResponseWrapper.getData}.
|
|
726
746
|
*
|
|
@@ -799,6 +819,8 @@ interface ApiBuilder extends ApiChainables {
|
|
|
799
819
|
delete<T = unknown>(path?: string): HttpRequest<"DELETE", T>;
|
|
800
820
|
/** Alias of `delete`. */
|
|
801
821
|
del<T = unknown>(path?: string): HttpRequest<"DELETE", T>;
|
|
822
|
+
/** Creates a QUERY request (RFC 10008): a safe, idempotent read whose query is sent with `withBody()`. */
|
|
823
|
+
query<T = unknown>(path?: string): HttpRequest<"QUERY", T>;
|
|
802
824
|
}
|
|
803
825
|
/**
|
|
804
826
|
* Creates an empty {@link ApiBuilder}. Configure it once, export it, and create every request through it.
|
|
@@ -821,9 +843,11 @@ export type PutRequest<T = unknown> = HttpRequest<"PUT", T>;
|
|
|
821
843
|
export type PatchRequest<T = unknown> = HttpRequest<"PATCH", T>;
|
|
822
844
|
/** A DELETE request. */
|
|
823
845
|
export type DeleteRequest<T = unknown> = HttpRequest<"DELETE", T>;
|
|
846
|
+
/** A QUERY request (RFC 10008). */
|
|
847
|
+
export type QueryRequest<T = unknown> = HttpRequest<"QUERY", T>;
|
|
824
848
|
/** Any request — the v1 name for {@link HttpRequest}. */
|
|
825
849
|
export type BaseRequest<T = unknown> = HttpRequest<Method, T>;
|
|
826
|
-
/** Any request that may carry a body — the v1 name for `HttpRequest<"POST" | "PUT" | "PATCH" | "DELETE">`. */
|
|
850
|
+
/** Any request that may carry a body — the v1 name for `HttpRequest<"POST" | "PUT" | "PATCH" | "DELETE" | "QUERY">`. */
|
|
827
851
|
export type BodyRequest<T = unknown> = HttpRequest<BodyMethod, T>;
|
|
828
852
|
/** Creates a GET request. `T` declares the JSON type the response is expected to have. */
|
|
829
853
|
export declare const createGet: <T = unknown>(url: string) => GetRequest<T>;
|
|
@@ -839,6 +863,17 @@ export declare const createPut: <T = unknown>(url: string) => PutRequest<T>;
|
|
|
839
863
|
export declare const createPatch: <T = unknown>(url: string) => PatchRequest<T>;
|
|
840
864
|
/** Creates a DELETE request. */
|
|
841
865
|
export declare const createDelete: <T = unknown>(url: string) => DeleteRequest<T>;
|
|
866
|
+
/**
|
|
867
|
+
* Creates a QUERY request ([RFC 10008](https://www.rfc-editor.org/rfc/rfc10008)): a safe, idempotent read like GET
|
|
868
|
+
* whose query travels in the body, so it can be as large and as structured as needed. The server must support the
|
|
869
|
+
* method and needs a `Content-Type`, which is set for you except for binary and stream bodies (use `withContentType()`).
|
|
870
|
+
*
|
|
871
|
+
* @example
|
|
872
|
+
* ```typescript
|
|
873
|
+
* const open = await create.query<Issue[]>("https://api.example.com/issues").withBody({ state: "open", labels: ["bug"] }).getJson();
|
|
874
|
+
* ```
|
|
875
|
+
*/
|
|
876
|
+
export declare const createQuery: <T = unknown>(url: string) => QueryRequest<T>;
|
|
842
877
|
/**
|
|
843
878
|
* The entry point: one factory per HTTP method plus `api()` for configured instances.
|
|
844
879
|
*
|
|
@@ -868,6 +903,8 @@ declare const create: {
|
|
|
868
903
|
readonly delete: <T = unknown>(url: string) => DeleteRequest<T>;
|
|
869
904
|
/** Alias of `delete`. */
|
|
870
905
|
readonly del: <T = unknown>(url: string) => DeleteRequest<T>;
|
|
906
|
+
/** Creates a QUERY request (RFC 10008): a safe, idempotent read whose query is sent with `withBody()` — see {@link createQuery}. */
|
|
907
|
+
readonly query: <T = unknown>(url: string) => QueryRequest<T>;
|
|
871
908
|
/** Creates an api instance — see {@link createApi}. */
|
|
872
909
|
readonly api: typeof createApi;
|
|
873
910
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -163,10 +163,13 @@ export declare class RequestError<TData = unknown> extends Error {
|
|
|
163
163
|
export declare const isRequestError: (error: unknown) => error is RequestError;
|
|
164
164
|
//#endregion
|
|
165
165
|
//#region src/types.d.ts
|
|
166
|
-
/**
|
|
167
|
-
|
|
166
|
+
/**
|
|
167
|
+
* The HTTP methods a request can be created with. `QUERY` ([RFC 10008](https://www.rfc-editor.org/rfc/rfc10008)) is a
|
|
168
|
+
* safe, idempotent read like `GET` whose query is sent in the body.
|
|
169
|
+
*/
|
|
170
|
+
type Method = "GET" | "HEAD" | "OPTIONS" | "DELETE" | "POST" | "PUT" | "PATCH" | "QUERY";
|
|
168
171
|
/** The HTTP methods that may carry a request body (`withBody` / `withGraphQL` are only available on these). */
|
|
169
|
-
type BodyMethod = "POST" | "PUT" | "PATCH" | "DELETE";
|
|
172
|
+
type BodyMethod = "POST" | "PUT" | "PATCH" | "DELETE" | "QUERY";
|
|
170
173
|
/** What `fetch` accepts as a body. */
|
|
171
174
|
type FetchBody = NonNullable<RequestInit["body"]>;
|
|
172
175
|
/** `"include"`, `"omit"` or `"same-origin"`. */
|
|
@@ -186,7 +189,8 @@ type PriorityHint = "auto" | "high" | "low";
|
|
|
186
189
|
*
|
|
187
190
|
* - `string` → sent as-is (`Content-Type: text/plain` unless you set one)
|
|
188
191
|
* - `Blob` / `File`, `FormData`, `URLSearchParams` → sent as-is, `fetch` sets the matching `Content-Type`
|
|
189
|
-
* - `ArrayBuffer`, typed arrays, `ReadableStream`
|
|
192
|
+
* - `ArrayBuffer`, typed arrays, `ReadableStream` and, in Node.js, Node streams and other async iterables → sent as-is,
|
|
193
|
+
* no `Content-Type` unless you set one
|
|
190
194
|
* - any other JSON-serialisable object or array → `JSON.stringify`-ed (`Content-Type: application/json` unless you set one)
|
|
191
195
|
*
|
|
192
196
|
* @example
|
|
@@ -237,6 +241,8 @@ interface RetryContext {
|
|
|
237
241
|
* 408, 425, 429, 500, 502, 503 or 504, with exponential backoff (300 ms, 600 ms, 1.2 s, … plus
|
|
238
242
|
* up to 100 ms of jitter, capped at `maxDelay`). A `Retry-After` header is honoured when present.
|
|
239
243
|
* Aborted requests and requests with a `ReadableStream` body are never retried, whatever the policy.
|
|
244
|
+
* With `getJson()`, `getText()` and the other readers an attempt includes reading the body, so a timeout
|
|
245
|
+
* during the read is retried too (`getResponse()` and `getBody()` hand the body over unread).
|
|
240
246
|
*
|
|
241
247
|
* @example
|
|
242
248
|
* ```typescript
|
|
@@ -261,14 +267,17 @@ interface RetryConfig {
|
|
|
261
267
|
delay?: number | ((context: RetryContext) => number) | undefined;
|
|
262
268
|
/** Statuses that are retried. Default: `[408, 425, 429, 500, 502, 503, 504]`. Network errors and timeouts are retried by default. */
|
|
263
269
|
statuses?: readonly number[] | undefined;
|
|
264
|
-
/** Methods that are retried. Default: all. Use `["GET", "HEAD", "OPTIONS", "PUT", "DELETE"]` to retry idempotent requests only. */
|
|
270
|
+
/** Methods that are retried. Default: all. Use `["GET", "HEAD", "OPTIONS", "QUERY", "PUT", "DELETE"]` to retry idempotent requests only. */
|
|
265
271
|
methods?: readonly Method[] | undefined;
|
|
266
272
|
/**
|
|
267
273
|
* Upper bound, in milliseconds, for the default backoff. A `Retry-After` header longer than this
|
|
268
274
|
* cancels the retry so you can react to it yourself. Default: `30000`.
|
|
269
275
|
*/
|
|
270
276
|
maxDelay?: number | undefined;
|
|
271
|
-
/**
|
|
277
|
+
/**
|
|
278
|
+
* Full override of the retry decision (`statuses` and `methods` are ignored). Aborts and stream bodies still never retry.
|
|
279
|
+
* It also receives the errors raised while a reader reads the body (`PARSE`, `VALIDATION`, `GRAPHQL`, with `status` set).
|
|
280
|
+
*/
|
|
272
281
|
shouldRetry?: ((context: RetryContext) => boolean | Promise<boolean>) | undefined;
|
|
273
282
|
/** Called before every retry, after the delay has been computed. Awaited if it returns a promise. */
|
|
274
283
|
onRetry?: ((context: RetryContext & {
|
|
@@ -327,6 +336,10 @@ type ResponseInterceptor = (response: ResponseWrapper, request: HttpRequest) =>
|
|
|
327
336
|
* Return nothing to keep the error, another {@link RequestError} to replace it, or a
|
|
328
337
|
* {@link ResponseWrapper} to recover — typically by replaying `request.clone()`. Throwing replaces the error as well.
|
|
329
338
|
*
|
|
339
|
+
* With `getJson()`, `getText()` and the other readers, a failure while reading the body (`PARSE`, `VALIDATION`,
|
|
340
|
+
* `GRAPHQL`, or a timeout or abort during the read) also reaches error interceptors, and a recovered
|
|
341
|
+
* `ResponseWrapper` is read the same way; if that read fails, the next interceptor receives the new error.
|
|
342
|
+
*
|
|
330
343
|
* @example
|
|
331
344
|
* ```typescript
|
|
332
345
|
* const replayed = new WeakSet<HttpRequest>();
|
|
@@ -388,23 +401,17 @@ type RequestResult<T> = {
|
|
|
388
401
|
* ```
|
|
389
402
|
*/
|
|
390
403
|
export declare class ResponseWrapper<T = unknown> {
|
|
404
|
+
private _bin?;
|
|
405
|
+
private _text?;
|
|
406
|
+
private _json?;
|
|
391
407
|
/** The underlying `Response`. Read its body through this wrapper's methods, or take it over with `getBody()`. */
|
|
392
408
|
readonly raw: Response;
|
|
393
409
|
/** The URL that was requested, including the query string (after interceptors). */
|
|
394
410
|
readonly url: string;
|
|
395
411
|
/** The HTTP method that was used. */
|
|
396
412
|
readonly method: Method;
|
|
397
|
-
private _bin?;
|
|
398
|
-
private _text?;
|
|
399
|
-
private _json?;
|
|
400
413
|
/** Wraps a `Response` — useful to hand a synthetic response to an error interceptor. */
|
|
401
|
-
constructor(
|
|
402
|
-
/** The underlying `Response`. Read its body through this wrapper's methods, or take it over with `getBody()`. */
|
|
403
|
-
raw: Response,
|
|
404
|
-
/** The URL that was requested, including the query string (after interceptors). */
|
|
405
|
-
url?: string,
|
|
406
|
-
/** The HTTP method that was used. */
|
|
407
|
-
method?: Method);
|
|
414
|
+
constructor(raw: Response, url?: string, method?: Method);
|
|
408
415
|
/** HTTP status code (`200`, `404`, …). */
|
|
409
416
|
get status(): number;
|
|
410
417
|
/** HTTP status text (`"OK"`, `"Not Found"`, …); empty over HTTP/2. */
|
|
@@ -430,6 +437,8 @@ export declare class ResponseWrapper<T = unknown> {
|
|
|
430
437
|
* The raw body stream, for streaming consumption (downloads with progress, SSE/NDJSON, LLM output…).
|
|
431
438
|
* Unlike the other readers it is not buffered: the body can be read once, and only if no other reader ran.
|
|
432
439
|
* Taking the stream also ends the request's `withTimeout()` deadline — from here on the stream is yours.
|
|
440
|
+
* It has the type `fetch` gives `response.body` in your environment (DOM lib or `@types/node`), so it pipes
|
|
441
|
+
* through `TextDecoderStream` and other web streams like that one does.
|
|
433
442
|
*
|
|
434
443
|
* @example
|
|
435
444
|
* ```typescript
|
|
@@ -437,7 +446,7 @@ export declare class ResponseWrapper<T = unknown> {
|
|
|
437
446
|
* for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) process(chunk.value);
|
|
438
447
|
* ```
|
|
439
448
|
*/
|
|
440
|
-
getBody():
|
|
449
|
+
getBody(): Response["body"];
|
|
441
450
|
/**
|
|
442
451
|
* The body parsed as JSON. An empty body (`204`, `Content-Length: 0`, whitespace) yields `null` —
|
|
443
452
|
* declare it in the type (`getJson<User | null>()`) for endpoints that may return nothing.
|
|
@@ -457,7 +466,8 @@ export declare class ResponseWrapper<T = unknown> {
|
|
|
457
466
|
/**
|
|
458
467
|
* `getJson()` followed by a selector, so you can pick the part you need in one call. The selector
|
|
459
468
|
* receives `T` (declare it on the request: `api.get<Page>()`), or pass both type arguments explicitly.
|
|
460
|
-
*
|
|
469
|
+
* A selector that throws (or returns a promise that rejects) is reported as a `RequestError` with code `"PARSE"`.
|
|
470
|
+
* A schema can be validated first.
|
|
461
471
|
*
|
|
462
472
|
* @example
|
|
463
473
|
* ```typescript
|
|
@@ -501,9 +511,7 @@ export declare class HttpRequest<M extends Method = Method, T = unknown> {
|
|
|
501
511
|
readonly method: M;
|
|
502
512
|
private readonly _url;
|
|
503
513
|
/** Prefer `create.get(url)`, `create.post(url)`, … or an api instance to construct requests. */
|
|
504
|
-
constructor(
|
|
505
|
-
/** The HTTP method. */
|
|
506
|
-
method: M, url: string);
|
|
514
|
+
constructor(method: M, url: string);
|
|
507
515
|
/** The URL this request will be sent to, including the query string. */
|
|
508
516
|
get url(): string;
|
|
509
517
|
private _fail;
|
|
@@ -648,14 +656,18 @@ export declare class HttpRequest<M extends Method = Method, T = unknown> {
|
|
|
648
656
|
withRequestInterceptor(interceptor: RequestInterceptor): this;
|
|
649
657
|
/** Adds a {@link ResponseInterceptor}, run after every successful response in registration order (it also receives the request). */
|
|
650
658
|
withResponseInterceptor(interceptor: ResponseInterceptor): this;
|
|
651
|
-
/**
|
|
659
|
+
/**
|
|
660
|
+
* Adds an {@link ErrorInterceptor}, run once after the request has failed for good (after retries, and including a failure while
|
|
661
|
+
* `getJson()`, `getText()`, … read the body); it also receives the request, so it can replay it.
|
|
662
|
+
*/
|
|
652
663
|
withErrorInterceptor(interceptor: ErrorInterceptor): this;
|
|
653
664
|
/**
|
|
654
|
-
* Sets the request body (POST, PUT, PATCH and
|
|
655
|
-
* arrays are JSON-encoded; strings, `Blob`, `FormData`, `URLSearchParams`, `ArrayBuffer`, typed arrays
|
|
656
|
-
*
|
|
657
|
-
*
|
|
658
|
-
* Stream bodies are sent with `duplex: "half"`
|
|
665
|
+
* Sets the request body (POST, PUT, PATCH, DELETE and QUERY only — a compile error elsewhere). Objects and
|
|
666
|
+
* arrays are JSON-encoded; strings, `Blob`, `FormData`, `URLSearchParams`, `ArrayBuffer`, typed arrays,
|
|
667
|
+
* `ReadableStream` and, in Node.js, Node streams (`fs.createReadStream()`) and other async iterables are sent
|
|
668
|
+
* as-is. `Content-Type` is set to `application/json` / `text/plain` unless already present, and removed for
|
|
669
|
+
* `FormData` (fetch must add the multipart boundary itself). Stream bodies are sent with `duplex: "half"`
|
|
670
|
+
* (Chromium and Node.js only), can be sent only once and are never retried.
|
|
659
671
|
*
|
|
660
672
|
* @example
|
|
661
673
|
* ```typescript
|
|
@@ -679,7 +691,7 @@ export declare class HttpRequest<M extends Method = Method, T = unknown> {
|
|
|
679
691
|
withGraphQL(this: HttpRequest<BodyMethod, T>, query: string, variables?: object, options?: GraphQLOptions): this;
|
|
680
692
|
/**
|
|
681
693
|
* An independent copy of this request, so a configured request can serve as a template.
|
|
682
|
-
* Interceptors, signals and body objects are shared by reference (a
|
|
694
|
+
* Interceptors, signals and body objects are shared by reference (a stream body can only be sent once).
|
|
683
695
|
*
|
|
684
696
|
* @example
|
|
685
697
|
* ```typescript
|
|
@@ -693,8 +705,16 @@ export declare class HttpRequest<M extends Method = Method, T = unknown> {
|
|
|
693
705
|
* Sends the request (with retries, if configured) and resolves with the {@link ResponseWrapper} of a
|
|
694
706
|
* successful (2xx or opaque) response. Any failure — non-2xx status, network error, timeout, abort,
|
|
695
707
|
* interceptor error — rejects with a {@link RequestError}; use `getResult()` for a non-throwing variant.
|
|
708
|
+
* It resolves before the body is read, so reading it is up to you and happens after the retries and error
|
|
709
|
+
* interceptors; `getJson()`, `getText()` and the other readers read it as part of each attempt instead.
|
|
696
710
|
*/
|
|
697
711
|
getResponse(): Promise<ResponseWrapper<T>>;
|
|
712
|
+
/**
|
|
713
|
+
* Runs the attempts, the retry policy and the error interceptors around `read`, which receives each response — so a
|
|
714
|
+
* failure while reading the body (a timeout during the read, invalid JSON, a schema or GraphQL error) is retried and
|
|
715
|
+
* intercepted like a failure before it. A response recovered by an error interceptor goes through `read` as well.
|
|
716
|
+
*/
|
|
717
|
+
private _run;
|
|
698
718
|
private _retriable;
|
|
699
719
|
private _attempt;
|
|
700
720
|
/** Attaches the CSRF token (see {@link withCsrf}) to `config.headers` when the final URL qualifies. */
|
|
@@ -720,7 +740,7 @@ export declare class HttpRequest<M extends Method = Method, T = unknown> {
|
|
|
720
740
|
/** Sends the request and parses the body as `FormData`. */
|
|
721
741
|
getFormData(): Promise<FormData>;
|
|
722
742
|
/** Sends the request and returns the raw body stream — see {@link ResponseWrapper.getBody}. */
|
|
723
|
-
getBody(): Promise<
|
|
743
|
+
getBody(): Promise<Response["body"]>;
|
|
724
744
|
/**
|
|
725
745
|
* Sends the request, parses JSON and applies a selector — see {@link ResponseWrapper.getData}.
|
|
726
746
|
*
|
|
@@ -799,6 +819,8 @@ interface ApiBuilder extends ApiChainables {
|
|
|
799
819
|
delete<T = unknown>(path?: string): HttpRequest<"DELETE", T>;
|
|
800
820
|
/** Alias of `delete`. */
|
|
801
821
|
del<T = unknown>(path?: string): HttpRequest<"DELETE", T>;
|
|
822
|
+
/** Creates a QUERY request (RFC 10008): a safe, idempotent read whose query is sent with `withBody()`. */
|
|
823
|
+
query<T = unknown>(path?: string): HttpRequest<"QUERY", T>;
|
|
802
824
|
}
|
|
803
825
|
/**
|
|
804
826
|
* Creates an empty {@link ApiBuilder}. Configure it once, export it, and create every request through it.
|
|
@@ -821,9 +843,11 @@ export type PutRequest<T = unknown> = HttpRequest<"PUT", T>;
|
|
|
821
843
|
export type PatchRequest<T = unknown> = HttpRequest<"PATCH", T>;
|
|
822
844
|
/** A DELETE request. */
|
|
823
845
|
export type DeleteRequest<T = unknown> = HttpRequest<"DELETE", T>;
|
|
846
|
+
/** A QUERY request (RFC 10008). */
|
|
847
|
+
export type QueryRequest<T = unknown> = HttpRequest<"QUERY", T>;
|
|
824
848
|
/** Any request — the v1 name for {@link HttpRequest}. */
|
|
825
849
|
export type BaseRequest<T = unknown> = HttpRequest<Method, T>;
|
|
826
|
-
/** Any request that may carry a body — the v1 name for `HttpRequest<"POST" | "PUT" | "PATCH" | "DELETE">`. */
|
|
850
|
+
/** Any request that may carry a body — the v1 name for `HttpRequest<"POST" | "PUT" | "PATCH" | "DELETE" | "QUERY">`. */
|
|
827
851
|
export type BodyRequest<T = unknown> = HttpRequest<BodyMethod, T>;
|
|
828
852
|
/** Creates a GET request. `T` declares the JSON type the response is expected to have. */
|
|
829
853
|
export declare const createGet: <T = unknown>(url: string) => GetRequest<T>;
|
|
@@ -839,6 +863,17 @@ export declare const createPut: <T = unknown>(url: string) => PutRequest<T>;
|
|
|
839
863
|
export declare const createPatch: <T = unknown>(url: string) => PatchRequest<T>;
|
|
840
864
|
/** Creates a DELETE request. */
|
|
841
865
|
export declare const createDelete: <T = unknown>(url: string) => DeleteRequest<T>;
|
|
866
|
+
/**
|
|
867
|
+
* Creates a QUERY request ([RFC 10008](https://www.rfc-editor.org/rfc/rfc10008)): a safe, idempotent read like GET
|
|
868
|
+
* whose query travels in the body, so it can be as large and as structured as needed. The server must support the
|
|
869
|
+
* method and needs a `Content-Type`, which is set for you except for binary and stream bodies (use `withContentType()`).
|
|
870
|
+
*
|
|
871
|
+
* @example
|
|
872
|
+
* ```typescript
|
|
873
|
+
* const open = await create.query<Issue[]>("https://api.example.com/issues").withBody({ state: "open", labels: ["bug"] }).getJson();
|
|
874
|
+
* ```
|
|
875
|
+
*/
|
|
876
|
+
export declare const createQuery: <T = unknown>(url: string) => QueryRequest<T>;
|
|
842
877
|
/**
|
|
843
878
|
* The entry point: one factory per HTTP method plus `api()` for configured instances.
|
|
844
879
|
*
|
|
@@ -868,6 +903,8 @@ declare const create: {
|
|
|
868
903
|
readonly delete: <T = unknown>(url: string) => DeleteRequest<T>;
|
|
869
904
|
/** Alias of `delete`. */
|
|
870
905
|
readonly del: <T = unknown>(url: string) => DeleteRequest<T>;
|
|
906
|
+
/** Creates a QUERY request (RFC 10008): a safe, idempotent read whose query is sent with `withBody()` — see {@link createQuery}. */
|
|
907
|
+
readonly query: <T = unknown>(url: string) => QueryRequest<T>;
|
|
871
908
|
/** Creates an api instance — see {@link createApi}. */
|
|
872
909
|
readonly api: typeof createApi;
|
|
873
910
|
};
|
package/dist/index.js
CHANGED
|
@@ -33,7 +33,7 @@ const messageOf = (error) => error instanceof Error ? error.message : String(err
|
|
|
33
33
|
|
|
34
34
|
//#endregion
|
|
35
35
|
//#region src/schema.ts
|
|
36
|
-
const isSchema = (value) =>
|
|
36
|
+
const isSchema = (value) => "~standard" in Object(value);
|
|
37
37
|
|
|
38
38
|
//#endregion
|
|
39
39
|
//#region src/utils.ts
|
|
@@ -123,9 +123,6 @@ const retryAfter = (response) => {
|
|
|
123
123
|
//#endregion
|
|
124
124
|
//#region src/response.ts
|
|
125
125
|
var ResponseWrapper = class {
|
|
126
|
-
raw;
|
|
127
|
-
url;
|
|
128
|
-
method;
|
|
129
126
|
constructor(raw, url = "", method = "GET") {
|
|
130
127
|
this.raw = raw;
|
|
131
128
|
this.url = url;
|
|
@@ -239,7 +236,7 @@ var ResponseWrapper = class {
|
|
|
239
236
|
const data = await this.getJson(schema);
|
|
240
237
|
if (!select) return data;
|
|
241
238
|
try {
|
|
242
|
-
return select(data);
|
|
239
|
+
return await select(data);
|
|
243
240
|
} catch (e) {
|
|
244
241
|
throw this._err(`Selector failed: ${messageOf(e)}`, "PARSE", { cause: e });
|
|
245
242
|
}
|
|
@@ -258,7 +255,6 @@ const RETRY_STATUSES = [
|
|
|
258
255
|
504
|
|
259
256
|
];
|
|
260
257
|
var HttpRequest = class HttpRequest {
|
|
261
|
-
method;
|
|
262
258
|
_o = {
|
|
263
259
|
init: {},
|
|
264
260
|
headers: {},
|
|
@@ -268,7 +264,6 @@ var HttpRequest = class HttpRequest {
|
|
|
268
264
|
res: [],
|
|
269
265
|
err: []
|
|
270
266
|
};
|
|
271
|
-
_url;
|
|
272
267
|
constructor(method, url) {
|
|
273
268
|
this.method = method;
|
|
274
269
|
this._url = url;
|
|
@@ -416,8 +411,8 @@ var HttpRequest = class HttpRequest {
|
|
|
416
411
|
withBody(body) {
|
|
417
412
|
const o = this._o;
|
|
418
413
|
const tag = Object.prototype.toString.call(body).slice(8, -1);
|
|
419
|
-
|
|
420
|
-
o.stream
|
|
414
|
+
o.stream = Symbol.asyncIterator in Object(body) || tag === "ReadableStream";
|
|
415
|
+
const raw = o.stream || ArrayBuffer.isView(body) || /^(String|Blob|File|FormData|URLSearchParams|ArrayBuffer)$/.test(tag);
|
|
421
416
|
if (raw) o.body = body;
|
|
422
417
|
else try {
|
|
423
418
|
o.body = JSON.stringify(body);
|
|
@@ -450,12 +445,15 @@ var HttpRequest = class HttpRequest {
|
|
|
450
445
|
};
|
|
451
446
|
return copy;
|
|
452
447
|
}
|
|
453
|
-
|
|
448
|
+
getResponse() {
|
|
449
|
+
return this._run((response) => response);
|
|
450
|
+
}
|
|
451
|
+
async _run(read) {
|
|
454
452
|
const o = this._o;
|
|
455
453
|
const retry = o.retry;
|
|
456
454
|
let error;
|
|
457
455
|
for (let attempt = 0;; attempt++) try {
|
|
458
|
-
return await this._attempt();
|
|
456
|
+
return await read(await this._attempt());
|
|
459
457
|
} catch (e) {
|
|
460
458
|
error = e instanceof RequestError ? e : this._fail(`Unexpected error: ${messageOf(e)}`, "NETWORK", { cause: e });
|
|
461
459
|
const context = {
|
|
@@ -485,7 +483,7 @@ var HttpRequest = class HttpRequest {
|
|
|
485
483
|
}
|
|
486
484
|
for (const interceptor of o.err) try {
|
|
487
485
|
const result = await interceptor(error, this);
|
|
488
|
-
if (result instanceof ResponseWrapper) return result;
|
|
486
|
+
if (result instanceof ResponseWrapper) return await read(result);
|
|
489
487
|
if (result) error = result;
|
|
490
488
|
} catch (e) {
|
|
491
489
|
error = e instanceof RequestError ? e : this._fail(`Error interceptor failed: ${messageOf(e)}`, "INTERCEPTOR", {
|
|
@@ -617,25 +615,25 @@ var HttpRequest = class HttpRequest {
|
|
|
617
615
|
if (token && !(header in config.headers)) config.headers[header] = token;
|
|
618
616
|
}
|
|
619
617
|
getJson(schema) {
|
|
620
|
-
return this.
|
|
618
|
+
return this._run((response) => response.getJson(schema));
|
|
621
619
|
}
|
|
622
620
|
getText() {
|
|
623
|
-
return this.
|
|
621
|
+
return this._run((response) => response.getText());
|
|
624
622
|
}
|
|
625
623
|
getBlob() {
|
|
626
|
-
return this.
|
|
624
|
+
return this._run((response) => response.getBlob());
|
|
627
625
|
}
|
|
628
626
|
getArrayBuffer() {
|
|
629
|
-
return this.
|
|
627
|
+
return this._run((response) => response.getArrayBuffer());
|
|
630
628
|
}
|
|
631
629
|
getFormData() {
|
|
632
|
-
return this.
|
|
630
|
+
return this._run((response) => response.getFormData());
|
|
633
631
|
}
|
|
634
632
|
getBody() {
|
|
635
|
-
return this.
|
|
633
|
+
return this._run((response) => response.getBody());
|
|
636
634
|
}
|
|
637
635
|
getData(schemaOrSelector, selector) {
|
|
638
|
-
return this.
|
|
636
|
+
return this._run((response) => response.getData(schemaOrSelector, selector));
|
|
639
637
|
}
|
|
640
638
|
async getResult(schema) {
|
|
641
639
|
try {
|
|
@@ -656,11 +654,9 @@ var HttpRequest = class HttpRequest {
|
|
|
656
654
|
//#region src/api.ts
|
|
657
655
|
const ABSOLUTE = /^([a-z][a-z0-9+.-]*:)?\/\//i;
|
|
658
656
|
var Api = class Api {
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
this._base = _base;
|
|
663
|
-
this._defaults = _defaults;
|
|
657
|
+
constructor(base = "", defaults = []) {
|
|
658
|
+
this._base = base;
|
|
659
|
+
this._defaults = defaults;
|
|
664
660
|
}
|
|
665
661
|
_with(fn) {
|
|
666
662
|
fn(new HttpRequest("GET", ""));
|
|
@@ -684,7 +680,8 @@ const METHODS = [
|
|
|
684
680
|
"POST",
|
|
685
681
|
"PUT",
|
|
686
682
|
"PATCH",
|
|
687
|
-
"DELETE"
|
|
683
|
+
"DELETE",
|
|
684
|
+
"QUERY"
|
|
688
685
|
];
|
|
689
686
|
let installed = false;
|
|
690
687
|
function createApi() {
|
|
@@ -711,6 +708,7 @@ const createPost = (url) => new HttpRequest("POST", url);
|
|
|
711
708
|
const createPut = (url) => new HttpRequest("PUT", url);
|
|
712
709
|
const createPatch = (url) => new HttpRequest("PATCH", url);
|
|
713
710
|
const createDelete = (url) => new HttpRequest("DELETE", url);
|
|
711
|
+
const createQuery = (url) => new HttpRequest("QUERY", url);
|
|
714
712
|
const create = {
|
|
715
713
|
get: createGet,
|
|
716
714
|
head: createHead,
|
|
@@ -720,8 +718,9 @@ const create = {
|
|
|
720
718
|
patch: createPatch,
|
|
721
719
|
delete: createDelete,
|
|
722
720
|
del: createDelete,
|
|
721
|
+
query: createQuery,
|
|
723
722
|
api: createApi
|
|
724
723
|
};
|
|
725
724
|
|
|
726
725
|
//#endregion
|
|
727
|
-
export { HttpRequest, RequestError, ResponseWrapper, createApi, createDelete, createGet, createHead, createOptions, createPatch, createPost, createPut, create as default, isRequestError };
|
|
726
|
+
export { HttpRequest, RequestError, ResponseWrapper, createApi, createDelete, createGet, createHead, createOptions, createPatch, createPost, createPut, createQuery, create as default, isRequestError };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-request",
|
|
3
|
-
"version": "2.1
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "A tiny, chainable, fully typed wrapper around fetch: retries, timeouts, cancellation, interceptors, api instances, schema validation and one error type",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|