From db1f7d8cd7fc13ebd53cc98cf4f263acc39b870d Mon Sep 17 00:00:00 2001 From: Jonty Sewell Date: Thu, 16 Jul 2026 15:32:09 +0100 Subject: [PATCH 1/6] Add support for RFC 10008 - HTTP QUERY - for which a payload body is allowed --- src/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.ts b/src/utils.ts index 3bc0dbf5..b1e3b54f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -8,7 +8,7 @@ import type { } from "./types.ts"; const payloadMethods = new Set( - Object.freeze(["PATCH", "POST", "PUT", "DELETE"]) + Object.freeze(["PATCH", "POST", "PUT", "DELETE", "QUERY"]) ); export function isPayloadMethod(method = "GET"): boolean { return payloadMethods.has(method.toUpperCase()); From a25e0b183f54eb766378deeb859a4f2bdf285408 Mon Sep 17 00:00:00 2001 From: Jonty Sewell Date: Thu, 16 Jul 2026 15:53:17 +0100 Subject: [PATCH 2/6] add QUERY to README.md as method with body --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bf99be5d..9ba168ef 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ If an object or a class with a `.toJSON()` method is passed to the `body` option `ofetch` utilizes `JSON.stringify()` to convert the passed object. Classes without a `.toJSON()` method have to be converted into a string value in advance before being passed to the `body` option. -For `PUT`, `PATCH`, and `POST` request methods, when a string or object body is set, `ofetch` adds the default `"content-type": "application/json"` and `accept: "application/json"` headers (which you can always override). +For `PUT`, `PATCH`, `POST` and `QUERY` request methods, when a string or object body is set, `ofetch` adds the default `"content-type": "application/json"` and `accept: "application/json"` headers (which you can always override). Additionally, `ofetch` supports binary responses with `Buffer`, `ReadableStream`, `Stream`, and [compatible body types](https://developer.mozilla.org/en-US/docs/Web/API/fetch#body). `ofetch` will automatically set the `duplex: "half"` option for streaming support! From 1cc413eabb5d1f39600d198e853143c42fcf4e62 Mon Sep 17 00:00:00 2001 From: Jonty Sewell Date: Thu, 16 Jul 2026 15:53:28 +0100 Subject: [PATCH 3/6] add simple test for QUERY with payload body --- test/index.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/index.test.ts b/test/index.test.ts index 5ac20b07..13b42c34 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -158,6 +158,21 @@ describe("ofetch", () => { } }); + it("stringifies QUERY body automatically", async () => { + const { body } = await $fetch(getURL("post"), { + method: "QUERY", + body: { query: "{ users { name } }" }, + }); + expect(body).to.deep.eq({ query: "{ users { name } }" }); + + const { headers } = await $fetch(getURL("post"), { + method: "QUERY", + body: { query: "{ users { name } }" }, + }); + expect(headers).to.include({ "content-type": "application/json" }); + expect(headers).to.include({ accept: "application/json" }); + }); + it("does not stringify body when content type != application/json", async () => { const message = '"Hallo von Pascal"'; const { body } = await $fetch(getURL("echo"), { From fb09a1814af65e3f40bf96e0b723d1c1a7498f6c Mon Sep 17 00:00:00 2001 From: Jonty Sewell Date: Thu, 16 Jul 2026 15:54:32 +0100 Subject: [PATCH 4/6] update retry logic to include QUERY method as safe and idempotent --- README.md | 2 +- src/fetch.ts | 3 ++- src/utils.ts | 10 ++++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9ba168ef..2f7c8383 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ await ofetch("/url", { ignoreResponseError: true }); You can specify the amount of retry and delay between them using `retry` and `retryDelay` options and also pass a custom array of codes using `retryStatusCodes` option. -The default for `retry` is `1` retry, except for `POST`, `PUT`, `PATCH`, and `DELETE` methods where `ofetch` does not retry by default to avoid introducing side effects. If you set a custom value for `retry` it will **always retry** for all requests. +The default for `retry` is `1` retry, except for `POST`, `PUT`, `PATCH`, and `DELETE` methods where `ofetch` does not retry by default to avoid introducing side effects. The `QUERY` method (which carries a payload but is safe and idempotent) does retry by default. If you set a custom value for `retry` it will **always retry** for all requests. The default for `retryDelay` is `0` ms. diff --git a/src/fetch.ts b/src/fetch.ts index 10c91583..40a1a696 100644 --- a/src/fetch.ts +++ b/src/fetch.ts @@ -3,6 +3,7 @@ import { withBase, withQuery } from "./utils.url.ts"; import { createFetchError } from "./error.ts"; import { isPayloadMethod, + isNonRetryableMethod, isJSONSerializable, detectResponseType, resolveFetchOptions, @@ -51,7 +52,7 @@ export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch { if (typeof context.options.retry === "number") { retries = context.options.retry; } else { - retries = isPayloadMethod(context.options.method) ? 0 : 1; + retries = isNonRetryableMethod(context.options.method) ? 0 : 1; } const responseCode = (context.response && context.response.status) || 500; diff --git a/src/utils.ts b/src/utils.ts index b1e3b54f..0b12ba8b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -14,6 +14,16 @@ export function isPayloadMethod(method = "GET"): boolean { return payloadMethods.has(method.toUpperCase()); } +// Payload methods that are NOT safe/idempotent — these won't retry by default +// to avoid unintended side effects. QUERY is excluded because it's defined as +// safe and idempotent per RFC 10008. +const nonRetryableMethods = new Set( + Object.freeze(["POST", "PUT", "PATCH", "DELETE"]) +); +export function isNonRetryableMethod(method = "GET"): boolean { + return nonRetryableMethods.has(method.toUpperCase()); +} + export function isJSONSerializable(value: any): boolean { if (value === undefined) { return false; From 2f70719b19f03dbd5b5a2172bf68563cafd852df Mon Sep 17 00:00:00 2001 From: Jonty Sewell Date: Thu, 16 Jul 2026 16:39:34 +0100 Subject: [PATCH 5/6] clarify behaviour of retry logic against different http methods --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2f7c8383..275b7721 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,15 @@ await ofetch("/url", { ignoreResponseError: true }); You can specify the amount of retry and delay between them using `retry` and `retryDelay` options and also pass a custom array of codes using `retryStatusCodes` option. -The default for `retry` is `1` retry, except for `POST`, `PUT`, `PATCH`, and `DELETE` methods where `ofetch` does not retry by default to avoid introducing side effects. The `QUERY` method (which carries a payload but is safe and idempotent) does retry by default. If you set a custom value for `retry` it will **always retry** for all requests. +The default retry behavior depends on the request method: + +- **Default** (`retry: 1`) — applies to `GET`, `HEAD`, `QUERY`, and other safe methods. +- **No retry by default** (`retry: 0`) — applies to `POST`, `PUT`, `PATCH`, and `DELETE` to avoid introducing side effects. + +You can override the default by setting `retry` explicitly: + +- A **positive number** overrides the method-specific default and will retry for all requests. +- **`false`** or **`0`** disables retries entirely. The default for `retryDelay` is `0` ms. From 5705032fa796b4f0fc4ef667d5dca763faa363c9 Mon Sep 17 00:00:00 2001 From: Jonty Sewell Date: Thu, 16 Jul 2026 16:42:26 +0100 Subject: [PATCH 6/6] update README.md to include DELETE method in body handling description --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 275b7721..7e22e024 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ If an object or a class with a `.toJSON()` method is passed to the `body` option `ofetch` utilizes `JSON.stringify()` to convert the passed object. Classes without a `.toJSON()` method have to be converted into a string value in advance before being passed to the `body` option. -For `PUT`, `PATCH`, `POST` and `QUERY` request methods, when a string or object body is set, `ofetch` adds the default `"content-type": "application/json"` and `accept: "application/json"` headers (which you can always override). +For `PUT`, `PATCH`, `POST`, `DELETE`, and `QUERY` request methods, when a string or object body is set, `ofetch` adds the default `"content-type": "application/json"` and `accept: "application/json"` headers (which you can always override). Additionally, `ofetch` supports binary responses with `Buffer`, `ReadableStream`, `Stream`, and [compatible body types](https://developer.mozilla.org/en-US/docs/Web/API/fetch#body). `ofetch` will automatically set the `duplex: "half"` option for streaming support!