Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support timeout and AbortController #268

Merged
merged 12 commits into from
Aug 22, 2023
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,16 @@ await ofetch("http://google.com/404", {
});
```

## βœ”οΈ Timeout

You can specify `timeout` in milliseconds to automatically abort request after a timeout (default is disabled).

```ts
await ofetch("http://google.com/404", {
timeout: 3000, // Timeout after 3 seconds
});
```

## βœ”οΈ Type Friendly

Response can be type assisted:
Expand Down
25 changes: 21 additions & 4 deletions src/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface CreateFetchOptions {
defaults?: FetchOptions;
fetch?: Fetch;
Headers?: typeof Headers;
AbortController?: typeof AbortController;
}

export type FetchRequest = RequestInfo;
Expand Down Expand Up @@ -45,7 +46,8 @@ export interface FetchOptions<R extends ResponseType = ResponseType>
responseType?: R;
response?: boolean;
retry?: number | false;

/** timeout in milliseconds */
timeout?: number;
/** Delay between retries in milliseconds. */
retryDelay?: number;

Expand Down Expand Up @@ -87,15 +89,21 @@ const retryStatusCodes = new Set([
]);

export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch {
const { fetch = globalThis.fetch, Headers = globalThis.Headers } =
globalOptions;
const {
fetch = globalThis.fetch,
Headers = globalThis.Headers,
AbortController = globalThis.AbortController,
} = globalOptions;

async function onError(context: FetchContext): Promise<FetchResponse<any>> {
// Is Abort
// If it is an active abort, it will not retry automatically.
// https://developer.mozilla.org/en-US/docs/Web/API/DOMException#error_names
const isAbort =
(context.error && context.error.name === "AbortError") || false;
(context.error &&
context.error.name === "AbortError" &&
!context.options.timeout) ||
false;
// Retry
if (context.options.retry !== false && !isAbort) {
let retries;
Expand All @@ -111,9 +119,11 @@ export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch {
if (retryDelay > 0) {
await new Promise((resolve) => setTimeout(resolve, retryDelay));
}
// Timeout
return $fetchRaw(context.request, {
...context.options,
retry: retries - 1,
timeout: context.options.timeout,
});
}
}
Expand Down Expand Up @@ -184,6 +194,13 @@ export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch {
}
}

// TODO: Can we merge signals?
if (!context.options.signal && context.options.timeout) {
const controller = new AbortController();
setTimeout(() => controller.abort(), context.options.timeout);
context.options.signal = controller.signal;
}

try {
context.response = await fetch(
context.request,
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const fetch =
(() => Promise.reject(new Error("[ofetch] global.fetch is not supported!")));

export const Headers = _globalThis.Headers;
export const AbortController = _globalThis.AbortController;

export const ofetch = createFetch({ fetch, Headers });
export const ofetch = createFetch({ fetch, Headers, AbortController });
export const $fetch = ofetch;
8 changes: 6 additions & 2 deletions src/node.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import http from "node:http";
import https, { AgentOptions } from "node:https";
import nodeFetch, { Headers as _Headers } from "node-fetch-native";
import nodeFetch, {
Headers as _Headers,
AbortController as _AbortController,
} from "node-fetch-native";

import { createFetch } from "./base";

Expand Down Expand Up @@ -33,6 +36,7 @@ export function createNodeFetch() {
export const fetch = globalThis.fetch || createNodeFetch();

export const Headers = globalThis.Headers || _Headers;
export const AbortController = globalThis.AbortController || _AbortController;

export const ofetch = createFetch({ fetch, Headers });
export const ofetch = createFetch({ fetch, Headers, AbortController });
export const $fetch = ofetch;
20 changes: 20 additions & 0 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,16 @@ describe("ofetch", () => {
.use(
"/408",
eventHandler(() => createError({ status: 408 }))
)
.use(
"/timeout",
eventHandler(async () => {
await new Promise((resolve) => {
setTimeout(() => {
resolve(createError({ status: 408 }));
}, 1000 * 5);
});
})
);

listener = await listen(toNodeListener(app));
Expand Down Expand Up @@ -229,6 +239,16 @@ describe("ofetch", () => {
expect(abortHandle()).rejects.toThrow(/aborted/);
});

it("aborting on timeout", async () => {
const noTimeout = $fetch(getURL("timeout")).catch(() => "no timeout");
const timeout = $fetch(getURL("timeout"), {
timeout: 100,
retry: 0,
}).catch(() => "timeout");
const race = await Promise.race([noTimeout, timeout]);
expect(race).to.equal("timeout");
});

it("deep merges defaultOptions", async () => {
const _customFetch = $fetch.create({
query: {
Expand Down