Skip to content

Commit e93bc6d

Browse files
authoredOct 24, 2023
Fix support with environments that add support for custom fetch options (#536)
1 parent d372e2e commit e93bc6d

File tree

6 files changed

+108
-49
lines changed

6 files changed

+108
-49
lines changed
 

‎source/core/Ky.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {normalizeRequestMethod, normalizeRetryOptions} from '../utils/normalize.
88
import timeout, {type TimeoutOptions} from '../utils/timeout.js';
99
import delay from '../utils/delay.js';
1010
import {type ObjectEntries} from '../utils/types.js';
11+
import {findUnknownOptions} from '../utils/options.js';
1112
import {
1213
maxSafeTimeout,
1314
responseTypes,
@@ -294,11 +295,13 @@ export class Ky {
294295
}
295296
}
296297

298+
const nonRequestOptions = findUnknownOptions(this.request, this._options);
299+
297300
if (this._options.timeout === false) {
298-
return this._options.fetch(this.request.clone());
301+
return this._options.fetch(this.request.clone(), nonRequestOptions);
299302
}
300303

301-
return timeout(this.request.clone(), this.abortController, this._options as TimeoutOptions);
304+
return timeout(this.request.clone(), nonRequestOptions, this.abortController, this._options as TimeoutOptions);
302305
}
303306

304307
/* istanbul ignore next */

‎source/core/constants.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type {Expect, Equal} from '@type-challenges/utils';
2-
import {type HttpMethod} from '../types/options.js';
2+
import {type HttpMethod, type KyOptionsRegistry} from '../types/options.js';
33

44
export const supportsRequestStreams = (() => {
55
let duplexAccessed = false;
@@ -45,3 +45,16 @@ export const responseTypes = {
4545
export const maxSafeTimeout = 2_147_483_647;
4646

4747
export const stop = Symbol('stop');
48+
49+
export const kyOptionKeys: KyOptionsRegistry = {
50+
json: true,
51+
parseJson: true,
52+
searchParams: true,
53+
prefixUrl: true,
54+
retry: true,
55+
timeout: true,
56+
hooks: true,
57+
throwHttpErrors: true,
58+
onDownloadProgress: true,
59+
fetch: true,
60+
};

‎source/types/options.ts

+58-45
Original file line numberDiff line numberDiff line change
@@ -25,53 +25,10 @@ export type DownloadProgress = {
2525
export type KyHeadersInit = HeadersInit | Record<string, string | undefined>;
2626

2727
/**
28-
Options are the same as `window.fetch`, with some exceptions.
28+
Custom Ky options
2929
*/
30-
export interface Options extends Omit<RequestInit, 'headers'> { // 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`.
31-
/**
32-
HTTP method used to make the request.
33-
34-
Internally, the standard methods (`GET`, `POST`, `PUT`, `PATCH`, `HEAD` and `DELETE`) are uppercased in order to avoid server errors due to case sensitivity.
35-
*/
36-
method?: LiteralUnion<HttpMethod, string>;
37-
38-
/**
39-
HTTP headers used to make the request.
40-
41-
You can pass a `Headers` instance or a plain object.
42-
43-
You can remove a header with `.extend()` by passing the header with an `undefined` value.
44-
45-
@example
46-
```
47-
import ky from 'ky';
48-
49-
const url = 'https://sindresorhus.com';
50-
51-
const original = ky.create({
52-
headers: {
53-
rainbow: 'rainbow',
54-
unicorn: 'unicorn'
55-
}
56-
});
57-
58-
const extended = original.extend({
59-
headers: {
60-
rainbow: undefined
61-
}
62-
});
63-
64-
const response = await extended(url).json();
65-
66-
console.log('rainbow' in response);
67-
//=> false
68-
69-
console.log('unicorn' in response);
70-
//=> true
71-
```
72-
*/
73-
headers?: KyHeadersInit;
7430

31+
export type KyOptions = {
7532
/**
7633
Shortcut for sending JSON. Use this instead of the `body` option.
7734
@@ -221,6 +178,62 @@ export interface Options extends Omit<RequestInit, 'headers'> { // eslint-disabl
221178
```
222179
*/
223180
fetch?: (input: RequestInfo, init?: RequestInit) => Promise<Response>;
181+
};
182+
183+
/**
184+
Each key from KyOptions is present and set to `true`.
185+
186+
This type is used for identifying and working with the known keys in KyOptions.
187+
*/
188+
export type KyOptionsRegistry = {[K in keyof KyOptions]-?: true};
189+
190+
/**
191+
Options are the same as `window.fetch`, except for the KyOptions
192+
*/
193+
export interface Options extends KyOptions, Omit<RequestInit, 'headers'> { // 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`.
194+
/**
195+
HTTP method used to make the request.
196+
197+
Internally, the standard methods (`GET`, `POST`, `PUT`, `PATCH`, `HEAD` and `DELETE`) are uppercased in order to avoid server errors due to case sensitivity.
198+
*/
199+
method?: LiteralUnion<HttpMethod, string>;
200+
201+
/**
202+
HTTP headers used to make the request.
203+
204+
You can pass a `Headers` instance or a plain object.
205+
206+
You can remove a header with `.extend()` by passing the header with an `undefined` value.
207+
208+
@example
209+
```
210+
import ky from 'ky';
211+
212+
const url = 'https://sindresorhus.com';
213+
214+
const original = ky.create({
215+
headers: {
216+
rainbow: 'rainbow',
217+
unicorn: 'unicorn'
218+
}
219+
});
220+
221+
const extended = original.extend({
222+
headers: {
223+
rainbow: undefined
224+
}
225+
});
226+
227+
const response = await extended(url).json();
228+
229+
console.log('rainbow' in response);
230+
//=> false
231+
232+
console.log('unicorn' in response);
233+
//=> true
234+
```
235+
*/
236+
headers?: KyHeadersInit;
224237
}
225238

226239
export type InternalOptions = Required<

‎source/utils/options.ts

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import {kyOptionKeys} from '../core/constants.js';
2+
3+
export const findUnknownOptions = (
4+
request: Request,
5+
options: Record<string, unknown>,
6+
): Record<string, unknown> => {
7+
const unknownOptions: Record<string, unknown> = {};
8+
9+
for (const key in options) {
10+
if (!(key in kyOptionKeys) && !(key in request)) {
11+
unknownOptions[key] = options[key];
12+
}
13+
}
14+
15+
return unknownOptions;
16+
};

‎source/utils/timeout.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export type TimeoutOptions = {
88
// `Promise.race()` workaround (#91)
99
export default async function timeout(
1010
request: Request,
11+
init: RequestInit,
1112
abortController: AbortController | undefined,
1213
options: TimeoutOptions,
1314
): Promise<Response> {
@@ -21,7 +22,7 @@ export default async function timeout(
2122
}, options.timeout);
2223

2324
void options
24-
.fetch(request)
25+
.fetch(request, init)
2526
.then(resolve)
2627
.catch(reject)
2728
.then(() => {

‎test/fetch.ts

+13
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,16 @@ test('options are correctly passed to Fetch #2', async t => {
6464
const json = await ky.post('https://httpbin.org/anything', {json: fixture}).json();
6565
t.deepEqual(json.json, fixture);
6666
});
67+
68+
test('unknown options are passed to fetch', async t => {
69+
t.plan(1);
70+
71+
const options = {next: {revalidate: 3600}};
72+
73+
const customFetch: typeof fetch = async (request, init) => {
74+
t.is(init.next, options.next);
75+
return new Response(request.url);
76+
};
77+
78+
await ky(fixture, {...options, fetch: customFetch}).text();
79+
});

0 commit comments

Comments
 (0)
Please sign in to comment.