From ccda3b98935ac26ffee70fb24d8e9fb28f89bbeb Mon Sep 17 00:00:00 2001 From: Skayo Date: Mon, 20 Jul 2020 21:57:37 +0200 Subject: [PATCH] Add option for custom `fetch` implementation (#273) * Overwrite globals.fetch when a 'fetch' option was provided * Fixed bug that wouldn't let me overwrite globals' properties * Fixed bug where 'this' referenced to the wrong object * Added basic documentation * Overwriting globals.fetch affects all other ky calls, therefore switched to this._options.fetch * Added basic tests * Formatting * More Formatting * Update index.d.ts * Update readme.md * Use shorthand syntax for passing fetch function in examples * Added more assertions to the test and removed .serial() * Added note that the user-defined fetch function has to be compatible with the standard * Fixed test * Plan for 12 assertions because every call for fetch is another assertion * Remove trailing comma * Remove configurable setting as it's not needed anymore Co-authored-by: Sindre Sorhus Co-authored-by: Seth Holladay --- index.d.ts | 24 ++++++++++++++++++++++++ index.js | 13 +++++++------ readme.md | 23 +++++++++++++++++++++++ test/fetch.js | 16 ++++++++++++++++ 4 files changed, 70 insertions(+), 6 deletions(-) diff --git a/index.d.ts b/index.d.ts index b871bad2..5e202426 100644 --- a/index.d.ts +++ b/index.d.ts @@ -332,6 +332,30 @@ export interface Options extends Omit { ``` */ onDownloadProgress?: (progress: DownloadProgress, chunk: Uint8Array) => void; + + /** + User-defined `fetch` function. + Has to be fully compatible with the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) standard. + + Use-cases: + 1. Use custom `fetch` implementations like [`isomorphic-unfetch`](https://www.npmjs.com/package/isomorphic-unfetch). + 2. Use the `fetch` wrapper function provided by some frameworks that use server-side rendering (SSR). + + @default fetch + + @example + ``` + import ky from 'ky'; + import fetch from 'isomorphic-unfetch'; + + (async () => { + const parsed = await ky('https://example.com', { + fetch + }).json(); + })(); + ``` + */ + fetch?: (input: RequestInfo, init?: RequestInit) => Promise; } /** diff --git a/index.js b/index.js index 53250348..5a5a51eb 100644 --- a/index.js +++ b/index.js @@ -166,7 +166,7 @@ class TimeoutError extends Error { const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); // `Promise.race()` workaround (#91) -const timeout = (request, ms, abortController) => +const timeout = (request, abortController, options) => new Promise((resolve, reject) => { const timeoutID = setTimeout(() => { if (abortController) { @@ -174,10 +174,10 @@ const timeout = (request, ms, abortController) => } reject(new TimeoutError(request)); - }, ms); + }, options.timeout); /* eslint-disable promise/prefer-await-to-then */ - globals.fetch(request) + options.fetch(request) .then(resolve) .catch(reject) .then(() => { @@ -239,7 +239,8 @@ class Ky { prefixUrl: String(options.prefixUrl || ''), retry: normalizeRetryOptions(options.retry), throwHttpErrors: options.throwHttpErrors !== false, - timeout: typeof options.timeout === 'undefined' ? 10000 : options.timeout + timeout: typeof options.timeout === 'undefined' ? 10000 : options.timeout, + fetch: options.fetch || globals.fetch }; if (typeof this._input !== 'string' && !(this._input instanceof URL || this._input instanceof globals.Request)) { @@ -449,10 +450,10 @@ class Ky { } if (this._options.timeout === false) { - return globals.fetch(this.request.clone()); + return this._options.fetch(this.request.clone()); } - return timeout(this.request.clone(), this._options.timeout, this.abortController); + return timeout(this.request.clone(), this.abortController, this._options); } /* istanbul ignore next */ diff --git a/readme.md b/readme.md index d9b73650..b84deb96 100644 --- a/readme.md +++ b/readme.md @@ -368,6 +368,29 @@ import bourne from '@hapijs/bourne'; })(); ``` +##### fetch + +Type: `Function`\ +Default: `fetch` + +User-defined `fetch` function. +Has to be fully compatible with the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) standard. + +Use-cases: +1. Use custom `fetch` implementations like [`isomorphic-unfetch`](https://www.npmjs.com/package/isomorphic-unfetch). +2. Use the `fetch` wrapper function provided by some frameworks that use server-side rendering (SSR). + +```js +import ky from 'ky'; +import fetch from 'isomorphic-unfetch'; + +(async () => { + const parsed = await ky('https://example.com', { + fetch + }).json(); +})(); +``` + ### ky.extend(defaultOptions) Create a new `ky` instance with some defaults overridden with your own. diff --git a/test/fetch.js b/test/fetch.js index d34cbe98..1c66da01 100644 --- a/test/fetch.js +++ b/test/fetch.js @@ -16,3 +16,19 @@ test.serial('relative URLs are passed to fetch unresolved', async t => { t.is(await ky('unicorn', {prefixUrl: '/api/'}).text(), '/api/unicorn'); global.fetch = originalFetch; }); + +test('fetch option takes a custom fetch function', async t => { + t.plan(12); + + const customFetch = async input => { + t.true(input instanceof Request); + return new Response(input.url); + }; + + t.is(await ky('/unicorn', {fetch: customFetch}).text(), '/unicorn'); + t.is(await ky('/unicorn', {fetch: customFetch, searchParams: {foo: 'bar'}}).text(), '/unicorn?foo=bar'); + t.is(await ky('/unicorn#hash', {fetch: customFetch, searchParams: 'foo'}).text(), '/unicorn?foo=#hash'); + t.is(await ky('/unicorn?old', {fetch: customFetch, searchParams: 'new'}).text(), '/unicorn?new='); + t.is(await ky('/unicorn?old#hash', {fetch: customFetch, searchParams: 'new'}).text(), '/unicorn?new=#hash'); + t.is(await ky('unicorn', {fetch: customFetch, prefixUrl: '/api/'}).text(), '/api/unicorn'); +});