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 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 `create.get`, …), and
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 / ReadableStream → sent as-is
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 duplicate
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; error interceptors run once, after the last 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. Stream bodies are never retried.
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.92 KB | 4.46 KB |
535
- | `import { createGet }` only | 4.32 KB | |
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) => !!value && (typeof value === "object" || typeof value === "function") && "~standard" in 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
- const raw = ArrayBuffer.isView(body) || /^(String|Blob|File|FormData|URLSearchParams|ArrayBuffer|ReadableStream)$/.test(tag);
421
- o.stream = tag === "ReadableStream";
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
- async getResponse() {
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.getResponse().then((response) => response.getJson(schema));
619
+ return this._run((response) => response.getJson(schema));
622
620
  }
623
621
  getText() {
624
- return this.getResponse().then((response) => response.getText());
622
+ return this._run((response) => response.getText());
625
623
  }
626
624
  getBlob() {
627
- return this.getResponse().then((response) => response.getBlob());
625
+ return this._run((response) => response.getBlob());
628
626
  }
629
627
  getArrayBuffer() {
630
- return this.getResponse().then((response) => response.getArrayBuffer());
628
+ return this._run((response) => response.getArrayBuffer());
631
629
  }
632
630
  getFormData() {
633
- return this.getResponse().then((response) => response.getFormData());
631
+ return this._run((response) => response.getFormData());
634
632
  }
635
633
  getBody() {
636
- return this.getResponse().then((response) => response.getBody());
634
+ return this._run((response) => response.getBody());
637
635
  }
638
636
  getData(schemaOrSelector, selector) {
639
- return this.getResponse().then((response) => response.getData(schemaOrSelector, selector));
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
- _base;
661
- _defaults;
662
- constructor(_base = "", _defaults = []) {
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
- /** The HTTP methods a request can be created with. */
167
- type Method = "GET" | "HEAD" | "OPTIONS" | "DELETE" | "POST" | "PUT" | "PATCH";
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` → sent as-is, no `Content-Type` unless you set one
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
- /** Full override of the retry decision (`statuses` and `methods` are ignored). Aborts and stream bodies still never retry. */
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(): ReadableStream<Uint8Array> | null;
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
- * Errors thrown by the selector are reported as a `RequestError` with code `"PARSE"`. A schema can be validated first.
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
- /** 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. */
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 DELETE only — a compile error elsewhere). Objects and
655
- * arrays are JSON-encoded; strings, `Blob`, `FormData`, `URLSearchParams`, `ArrayBuffer`, typed arrays
656
- * and `ReadableStream` are sent as-is. `Content-Type` is set to `application/json` / `text/plain`
657
- * unless already present, and removed for `FormData` (fetch must add the multipart boundary itself).
658
- * Stream bodies are sent with `duplex: "half"` (Chromium and Node.js only) and are never retried.
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 `ReadableStream` body can only be sent once).
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<ReadableStream<Uint8Array> | null>;
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
- /** The HTTP methods a request can be created with. */
167
- type Method = "GET" | "HEAD" | "OPTIONS" | "DELETE" | "POST" | "PUT" | "PATCH";
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` → sent as-is, no `Content-Type` unless you set one
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
- /** Full override of the retry decision (`statuses` and `methods` are ignored). Aborts and stream bodies still never retry. */
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(): ReadableStream<Uint8Array> | null;
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
- * Errors thrown by the selector are reported as a `RequestError` with code `"PARSE"`. A schema can be validated first.
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
- /** 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. */
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 DELETE only — a compile error elsewhere). Objects and
655
- * arrays are JSON-encoded; strings, `Blob`, `FormData`, `URLSearchParams`, `ArrayBuffer`, typed arrays
656
- * and `ReadableStream` are sent as-is. `Content-Type` is set to `application/json` / `text/plain`
657
- * unless already present, and removed for `FormData` (fetch must add the multipart boundary itself).
658
- * Stream bodies are sent with `duplex: "half"` (Chromium and Node.js only) and are never retried.
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 `ReadableStream` body can only be sent once).
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<ReadableStream<Uint8Array> | null>;
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) => !!value && (typeof value === "object" || typeof value === "function") && "~standard" in 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
- const raw = ArrayBuffer.isView(body) || /^(String|Blob|File|FormData|URLSearchParams|ArrayBuffer|ReadableStream)$/.test(tag);
420
- o.stream = tag === "ReadableStream";
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
- async getResponse() {
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.getResponse().then((response) => response.getJson(schema));
618
+ return this._run((response) => response.getJson(schema));
621
619
  }
622
620
  getText() {
623
- return this.getResponse().then((response) => response.getText());
621
+ return this._run((response) => response.getText());
624
622
  }
625
623
  getBlob() {
626
- return this.getResponse().then((response) => response.getBlob());
624
+ return this._run((response) => response.getBlob());
627
625
  }
628
626
  getArrayBuffer() {
629
- return this.getResponse().then((response) => response.getArrayBuffer());
627
+ return this._run((response) => response.getArrayBuffer());
630
628
  }
631
629
  getFormData() {
632
- return this.getResponse().then((response) => response.getFormData());
630
+ return this._run((response) => response.getFormData());
633
631
  }
634
632
  getBody() {
635
- return this.getResponse().then((response) => response.getBody());
633
+ return this._run((response) => response.getBody());
636
634
  }
637
635
  getData(schemaOrSelector, selector) {
638
- return this.getResponse().then((response) => response.getData(schemaOrSelector, selector));
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
- _base;
660
- _defaults;
661
- constructor(_base = "", _defaults = []) {
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.0",
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,