From e93bc6dc455d2d84d99a6986d51829f2bf7f3d93 Mon Sep 17 00:00:00 2001 From: Kelvin Del Monte Date: Tue, 24 Oct 2023 10:39:01 -0400 Subject: [PATCH] Fix support with environments that add support for custom `fetch` options (#536) --- source/core/Ky.ts | 7 ++- source/core/constants.ts | 15 +++++- source/types/options.ts | 103 ++++++++++++++++++++++----------------- source/utils/options.ts | 16 ++++++ source/utils/timeout.ts | 3 +- test/fetch.ts | 13 +++++ 6 files changed, 108 insertions(+), 49 deletions(-) create mode 100644 source/utils/options.ts diff --git a/source/core/Ky.ts b/source/core/Ky.ts index a987f032..673e7e85 100644 --- a/source/core/Ky.ts +++ b/source/core/Ky.ts @@ -8,6 +8,7 @@ import {normalizeRequestMethod, normalizeRetryOptions} from '../utils/normalize. import timeout, {type TimeoutOptions} from '../utils/timeout.js'; import delay from '../utils/delay.js'; import {type ObjectEntries} from '../utils/types.js'; +import {findUnknownOptions} from '../utils/options.js'; import { maxSafeTimeout, responseTypes, @@ -294,11 +295,13 @@ export class Ky { } } + const nonRequestOptions = findUnknownOptions(this.request, this._options); + if (this._options.timeout === false) { - return this._options.fetch(this.request.clone()); + return this._options.fetch(this.request.clone(), nonRequestOptions); } - return timeout(this.request.clone(), this.abortController, this._options as TimeoutOptions); + return timeout(this.request.clone(), nonRequestOptions, this.abortController, this._options as TimeoutOptions); } /* istanbul ignore next */ diff --git a/source/core/constants.ts b/source/core/constants.ts index e162bc01..bbf43106 100644 --- a/source/core/constants.ts +++ b/source/core/constants.ts @@ -1,5 +1,5 @@ import type {Expect, Equal} from '@type-challenges/utils'; -import {type HttpMethod} from '../types/options.js'; +import {type HttpMethod, type KyOptionsRegistry} from '../types/options.js'; export const supportsRequestStreams = (() => { let duplexAccessed = false; @@ -45,3 +45,16 @@ export const responseTypes = { export const maxSafeTimeout = 2_147_483_647; export const stop = Symbol('stop'); + +export const kyOptionKeys: KyOptionsRegistry = { + json: true, + parseJson: true, + searchParams: true, + prefixUrl: true, + retry: true, + timeout: true, + hooks: true, + throwHttpErrors: true, + onDownloadProgress: true, + fetch: true, +}; diff --git a/source/types/options.ts b/source/types/options.ts index 4855d48e..79e62b0e 100644 --- a/source/types/options.ts +++ b/source/types/options.ts @@ -25,53 +25,10 @@ export type DownloadProgress = { export type KyHeadersInit = HeadersInit | Record; /** -Options are the same as `window.fetch`, with some exceptions. +Custom Ky options */ -export interface Options extends Omit { // eslint-disable-line @typescript-eslint/consistent-type-definitions -- This must stay an interface so that it can be extended outside of Ky for use in `ky.create`. - /** - HTTP method used to make the request. - - Internally, the standard methods (`GET`, `POST`, `PUT`, `PATCH`, `HEAD` and `DELETE`) are uppercased in order to avoid server errors due to case sensitivity. - */ - method?: LiteralUnion; - - /** - HTTP headers used to make the request. - - You can pass a `Headers` instance or a plain object. - - You can remove a header with `.extend()` by passing the header with an `undefined` value. - - @example - ``` - import ky from 'ky'; - - const url = 'https://sindresorhus.com'; - - const original = ky.create({ - headers: { - rainbow: 'rainbow', - unicorn: 'unicorn' - } - }); - - const extended = original.extend({ - headers: { - rainbow: undefined - } - }); - - const response = await extended(url).json(); - - console.log('rainbow' in response); - //=> false - - console.log('unicorn' in response); - //=> true - ``` - */ - headers?: KyHeadersInit; +export type KyOptions = { /** Shortcut for sending JSON. Use this instead of the `body` option. @@ -221,6 +178,62 @@ export interface Options extends Omit { // eslint-disabl ``` */ fetch?: (input: RequestInfo, init?: RequestInit) => Promise; +}; + +/** +Each key from KyOptions is present and set to `true`. + +This type is used for identifying and working with the known keys in KyOptions. +*/ +export type KyOptionsRegistry = {[K in keyof KyOptions]-?: true}; + +/** +Options are the same as `window.fetch`, except for the KyOptions +*/ +export interface Options extends KyOptions, Omit { // eslint-disable-line @typescript-eslint/consistent-type-definitions -- This must stay an interface so that it can be extended outside of Ky for use in `ky.create`. + /** + HTTP method used to make the request. + + Internally, the standard methods (`GET`, `POST`, `PUT`, `PATCH`, `HEAD` and `DELETE`) are uppercased in order to avoid server errors due to case sensitivity. + */ + method?: LiteralUnion; + + /** + HTTP headers used to make the request. + + You can pass a `Headers` instance or a plain object. + + You can remove a header with `.extend()` by passing the header with an `undefined` value. + + @example + ``` + import ky from 'ky'; + + const url = 'https://sindresorhus.com'; + + const original = ky.create({ + headers: { + rainbow: 'rainbow', + unicorn: 'unicorn' + } + }); + + const extended = original.extend({ + headers: { + rainbow: undefined + } + }); + + const response = await extended(url).json(); + + console.log('rainbow' in response); + //=> false + + console.log('unicorn' in response); + //=> true + ``` + */ + headers?: KyHeadersInit; } export type InternalOptions = Required< diff --git a/source/utils/options.ts b/source/utils/options.ts new file mode 100644 index 00000000..22c11d0a --- /dev/null +++ b/source/utils/options.ts @@ -0,0 +1,16 @@ +import {kyOptionKeys} from '../core/constants.js'; + +export const findUnknownOptions = ( + request: Request, + options: Record, +): Record => { + const unknownOptions: Record = {}; + + for (const key in options) { + if (!(key in kyOptionKeys) && !(key in request)) { + unknownOptions[key] = options[key]; + } + } + + return unknownOptions; +}; diff --git a/source/utils/timeout.ts b/source/utils/timeout.ts index 2ac49bd7..7b8cd130 100644 --- a/source/utils/timeout.ts +++ b/source/utils/timeout.ts @@ -8,6 +8,7 @@ export type TimeoutOptions = { // `Promise.race()` workaround (#91) export default async function timeout( request: Request, + init: RequestInit, abortController: AbortController | undefined, options: TimeoutOptions, ): Promise { @@ -21,7 +22,7 @@ export default async function timeout( }, options.timeout); void options - .fetch(request) + .fetch(request, init) .then(resolve) .catch(reject) .then(() => { diff --git a/test/fetch.ts b/test/fetch.ts index 4cc94dd3..05712087 100644 --- a/test/fetch.ts +++ b/test/fetch.ts @@ -64,3 +64,16 @@ test('options are correctly passed to Fetch #2', async t => { const json = await ky.post('https://httpbin.org/anything', {json: fixture}).json(); t.deepEqual(json.json, fixture); }); + +test('unknown options are passed to fetch', async t => { + t.plan(1); + + const options = {next: {revalidate: 3600}}; + + const customFetch: typeof fetch = async (request, init) => { + t.is(init.next, options.next); + return new Response(request.url); + }; + + await ky(fixture, {...options, fetch: customFetch}).text(); +});