Skip to content

Commit

Permalink
refactor: simplify
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 committed Aug 22, 2023
1 parent b767af1 commit ba43ba1
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 119 deletions.
121 changes: 60 additions & 61 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,16 @@ Import:

```js
// ESM / Typescript
import { ofetch } from 'ofetch'
import { ofetch } from "ofetch";

// CommonJS
const { ofetch } = require('ofetch')
const { ofetch } = require("ofetch");
```

## ✔️ Works with Node.js

We use [conditional exports](https://nodejs.org/api/packages.html#packages_conditional_exports) to detect Node.js
and automatically use [unjs/node-fetch-native](https://github.com/unjs/node-fetch-native). If `globalThis.fetch` is available, will be used instead. To leverage Node.js 17.5.0 experimental native fetch API use [`--experimental-fetch` flag](https://nodejs.org/dist/latest-v17.x/docs/api/cli.html#--experimental-fetch).
and automatically use [unjs/node-fetch-native](https://github.com/unjs/node-fetch-native). If `globalThis.fetch` is available, will be used instead. To leverage Node.js 17.5.0 experimental native fetch API use [`--experimental-fetch` flag](https://nodejs.org/dist/latest-v17.x/docs/api/cli.html#--experimental-fetch).

### `keepAlive` support

Expand All @@ -47,7 +47,7 @@ By setting the `FETCH_KEEP_ALIVE` environment variable to `true`, an http/https
`ofetch` will smartly parse JSON and native values using [destr](https://github.com/unjs/destr), falling back to text if it fails to parse.

```js
const { users } = await ofetch('/api/users')
const { users } = await ofetch("/api/users");
```

For binary content types, `ofetch` will instead return a `Blob` object.
Expand All @@ -56,21 +56,24 @@ You can optionally provide a different parser than destr, or specify `blob`, `ar

```js
// Use JSON.parse
await ofetch('/movie?lang=en', { parseResponse: JSON.parse })
await ofetch("/movie?lang=en", { parseResponse: JSON.parse });

// Return text as is
await ofetch('/movie?lang=en', { parseResponse: txt => txt })
await ofetch("/movie?lang=en", { parseResponse: (txt) => txt });

// Get the blob version of the response
await ofetch('/api/generate-image', { responseType: 'blob' })
await ofetch("/api/generate-image", { responseType: "blob" });
```

## ✔️ JSON Body

`ofetch` automatically stringifies request body (if an object is passed) and adds JSON `Content-Type` and `Accept` headers (for `put`, `patch` and `post` requests).

```js
const { users } = await ofetch('/api/users', { method: 'POST', body: { some: 'json' } })
const { users } = await ofetch("/api/users", {
method: "POST",
body: { some: "json" },
});
```

## ✔️ Handling Errors
Expand All @@ -80,21 +83,21 @@ const { users } = await ofetch('/api/users', { method: 'POST', body: { some: 'js
Parsed error body is available with `error.data`. You may also use `FetchError` type.

```ts
await ofetch('http://google.com/404')
await ofetch("http://google.com/404");
// FetchError: 404 Not Found (http://google.com/404)
// at async main (/project/playground.ts:4:3)
```

To catch error response:

```ts
await ofetch('/url').catch(err => err.data)
await ofetch("/url").catch((err) => err.data);
```

To bypass status error catching you can set `ignoreResponseError` option:

```ts
await ofetch('/url', { ignoreResponseError: true })
await ofetch("/url", { ignoreResponseError: true });
```

## ✔️ Auto Retry
Expand All @@ -119,37 +122,28 @@ Default for `retry` is `1` retry, except for `POST`, `PUT`, `PATCH` and `DELETE`
Default for `retryDelay` is zero ms.

```ts
await ofetch('http://google.com/404', {
await ofetch("http://google.com/404", {
retry: 3,
retryDelay: 500 // ms
})
retryDelay: 500, // ms
});
```

## ✔️ Timeout

You can specify `timeout` in milliseconds. Default for `timeout` is `undefined`, `0` also excluded as disabled 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, // ms
})
```

You can specify `timeoutExponent` function to control increase of timeout over retries:

```ts
await ofetch('http://google.com/404', {
timeout: 3000, // ms
timeoutExponent: (ms) => ms // this is default function
})
await ofetch("http://google.com/404", {
timeout: 3000, // Timeout after 3 seconds
});
```

## ✔️ Type Friendly

Response can be type assisted:

```ts
const article = await ofetch<Article>(`/api/article/${id}`)
const article = await ofetch<Article>(`/api/article/${id}`);
// Auto complete working with article.id
```

Expand All @@ -158,15 +152,15 @@ const article = await ofetch<Article>(`/api/article/${id}`)
By using `baseURL` option, `ofetch` prepends it with respecting to trailing/leading slashes and query search params for baseURL using [ufo](https://github.com/unjs/ufo):

```js
await ofetch('/config', { baseURL })
await ofetch("/config", { baseURL });
```

## ✔️ Adding Query Search Params

By using `query` option (or `params` as alias), `ofetch` adds query search params to URL by preserving query in request itself using [ufo](https://github.com/unjs/ufo):

```js
await ofetch('/movie?lang=en', { query: { id: 123 } })
await ofetch("/movie?lang=en", { query: { id: 123 } });
```

## ✔️ Interceptors
Expand All @@ -180,56 +174,60 @@ You might want to use `ofetch.create` to set shared interceptors.
`onRequest` is called as soon as `ofetch` is being called, allowing to modify options or just do simple logging.

```js
await ofetch('/api', {
await ofetch("/api", {
async onRequest({ request, options }) {
// Log request
console.log('[fetch request]', request, options)
console.log("[fetch request]", request, options);

// Add `?t=1640125211170` to query search params
options.query = options.query || {}
options.query.t = new Date()
}
})
options.query = options.query || {};
options.query.t = new Date();
},
});
```

### `onRequestError({ request, options, error })`

`onRequestError` will be called when fetch request fails.

```js
await ofetch('/api', {
await ofetch("/api", {
async onRequestError({ request, options, error }) {
// Log error
console.log('[fetch request error]', request, error)
}
})
console.log("[fetch request error]", request, error);
},
});
```


### `onResponse({ request, options, response })`

`onResponse` will be called after `fetch` call and parsing body.

```js
await ofetch('/api', {
await ofetch("/api", {
async onResponse({ request, response, options }) {
// Log response
console.log('[fetch response]', request, response.status, response.body)
}
})
console.log("[fetch response]", request, response.status, response.body);
},
});
```

### `onResponseError({ request, options, response })`

`onResponseError` is same as `onResponse` but will be called when fetch happens but `response.ok` is not `true`.

```js
await ofetch('/api', {
await ofetch("/api", {
async onResponseError({ request, response, options }) {
// Log error
console.log('[fetch response error]', request, response.status, response.body)
}
})
console.log(
"[fetch response error]",
request,
response.status,
response.body
);
},
});
```

## ✔️ Create fetch with default options
Expand All @@ -239,22 +237,22 @@ This utility is useful if you need to use common options across several fetch ca
**Note:** Defaults will be cloned at one level and inherited. Be careful about nested options like `headers`.

```js
const apiFetch = ofetch.create({ baseURL: '/api' })
const apiFetch = ofetch.create({ baseURL: "/api" });

apiFetch('/test') // Same as ofetch('/test', { baseURL: '/api' })
apiFetch("/test"); // Same as ofetch('/test', { baseURL: '/api' })
```

## 💡 Adding headers

By using `headers` option, `ofetch` adds extra headers in addition to the request default headers:

```js
await ofetch('/movies', {
await ofetch("/movies", {
headers: {
Accept: 'application/json',
'Cache-Control': 'no-cache'
}
})
Accept: "application/json",
"Cache-Control": "no-cache",
},
});
```

## 💡 Adding HTTP(S) Agent
Expand All @@ -264,17 +262,17 @@ If you need use HTTP(S) Agent, can add `agent` option with `https-proxy-agent` (
```js
import { HttpsProxyAgent } from "https-proxy-agent";

await ofetch('/api', {
agent: new HttpsProxyAgent('http://example.com')
})
await ofetch("/api", {
agent: new HttpsProxyAgent("http://example.com"),
});
```

## 🍣 Access to Raw Response

If you need to access raw response (for headers, etc), can use `ofetch.raw`:

```js
const response = await ofetch.raw('/sushi')
const response = await ofetch.raw("/sushi");

// response._data
// response.headers
Expand All @@ -286,7 +284,7 @@ const response = await ofetch.raw('/sushi')
As a shortcut, you can use `ofetch.native` that provides native `fetch` API

```js
const json = await ofetch.native('/sushi').then(r => r.json())
const json = await ofetch.native("/sushi").then((r) => r.json());
```

## 📦 Bundler Notes
Expand Down Expand Up @@ -319,6 +317,7 @@ If you need to support legacy users, you can optionally transpile the library in
MIT. Made with 💖

<!-- Badges -->

[npm-version-src]: https://img.shields.io/npm/v/ofetch?style=flat&colorA=18181B&colorB=F0DB4F
[npm-version-href]: https://npmjs.com/package/ofetch
[npm-downloads-src]: https://img.shields.io/npm/dm/ofetch?style=flat&colorA=18181B&colorB=F0DB4F
Expand Down
15 changes: 3 additions & 12 deletions src/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,8 @@ export interface FetchOptions<R extends ResponseType = ResponseType>
responseType?: R;
response?: boolean;
retry?: number | false;
/** timeout in milliseconds */
timeout?: number;
/** Function to increase timeout over retries */
timeoutExponent?: (ms: number) => number;

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

Expand Down Expand Up @@ -117,15 +115,10 @@ export function createFetch(globalOptions: CreateFetchOptions): $Fetch {
await new Promise((resolve) => setTimeout(resolve, retryDelay));
}
// Timeout
const timeout = context.options.timeout || 0;
if (typeof context.options.timeoutExponent !== "function") {
context.options.timeoutExponent = (ms: number) => ms;
}

return $fetchRaw(context.request, {
...context.options,
retry: retries - 1,
timeout: context.options.timeoutExponent(timeout),
timeout: context.options.timeout,
});
}
}
Expand Down Expand Up @@ -196,11 +189,9 @@ export function createFetch(globalOptions: CreateFetchOptions): $Fetch {
}
}

if (context.options.timeout && context.options.timeout > 0) {
if (!context.options.signal && context.options.timeout) {
const controller = new AbortController();

setTimeout(() => controller.abort(), context.options.timeout);

context.options.signal = controller.signal;
}

Expand Down
47 changes: 1 addition & 46 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,59 +241,14 @@ describe("ofetch", () => {

it("aborting on timeout", async () => {
const noTimeout = $fetch(getURL("timeout")).catch(() => "no timeout");

const timeout = $fetch(getURL("timeout"), {
timeout: 1000,
timeout: 100,
retry: 0,
}).catch(() => "timeout");

const race = await Promise.race([noTimeout, timeout]);

expect(race).to.equal("timeout");
});

it("timeout exponentially increasing over retries", async () => {
const timeoutWithRetries = $fetch(getURL("timeout"), {
timeout: 1000,
timeoutExponent: (v) => 2 * v,
retry: 1,
onRequestError(context) {
context.error = new Error(`${context.options.timeout}`);
},
}).catch((error) => Number.parseInt(error.message));

const timeoutNoRetries = $fetch(getURL("timeout"), {
timeout: 1000,
retry: 0,
onRequestError(context) {
context.error = new Error(`${context.options.timeout}`);
},
}).catch((error) => Number.parseInt(error.message));

const signalRace = await Promise.all([
timeoutNoRetries,
timeoutWithRetries,
]);

expect(signalRace[0]).to.equal(1000);
expect(signalRace[1]).to.equal(2000);
});

it("abort with timeout", () => {
const controller = new AbortController();
async function abortHandle() {
controller.abort();
const response = await $fetch(getURL("timeout"), {
timeout: 1000,
timeoutExponent: (v) => 2 * v,
retry: 1,
signal: controller.signal,
});
console.log(response);
}
expect(abortHandle()).rejects.toThrow(/aborted/);
});

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

0 comments on commit ba43ba1

Please sign in to comment.