Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

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!

Expand Down Expand Up @@ -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.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

The default for `retryDelay` is `0` ms.

Expand Down
3 changes: 2 additions & 1 deletion src/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { withBase, withQuery } from "./utils.url.ts";
import { createFetchError } from "./error.ts";
import {
isPayloadMethod,
isNonRetryableMethod,
isJSONSerializable,
detectResponseType,
resolveFetchOptions,
Expand Down Expand Up @@ -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;
Expand Down
12 changes: 11 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,22 @@ import type {
} from "./types.ts";

const payloadMethods = new Set(
Object.freeze(["PATCH", "POST", "PUT", "DELETE"])
Object.freeze(["PATCH", "POST", "PUT", "DELETE", "QUERY"])
Comment thread
coderabbitai[bot] marked this conversation as resolved.
);
Comment thread
jonty-comp marked this conversation as resolved.
export function isPayloadMethod(method = "GET"): boolean {
return payloadMethods.has(method.toUpperCase());
Comment thread
jonty-comp marked this conversation as resolved.
}

// 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;
Expand Down
15 changes: 15 additions & 0 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"), {
Expand Down