Skip to content

Commit

Permalink
Fix support with environments that add support for custom fetch opt…
Browse files Browse the repository at this point in the history
…ions (#536)
  • Loading branch information
kdelmonte committed Oct 24, 2023
1 parent d372e2e commit e93bc6d
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 49 deletions.
7 changes: 5 additions & 2 deletions source/core/Ky.ts
Expand Up @@ -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,
Expand Down Expand Up @@ -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 */
Expand Down
15 changes: 14 additions & 1 deletion 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;
Expand Down Expand Up @@ -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,
};
103 changes: 58 additions & 45 deletions source/types/options.ts
Expand Up @@ -25,53 +25,10 @@ export type DownloadProgress = {
export type KyHeadersInit = HeadersInit | Record<string, string | undefined>;

/**
Options are the same as `window.fetch`, with some exceptions.
Custom Ky options
*/
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`.
/**
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<HttpMethod, string>;

/**
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.
Expand Down Expand Up @@ -221,6 +178,62 @@ export interface Options extends Omit<RequestInit, 'headers'> { // eslint-disabl
```
*/
fetch?: (input: RequestInfo, init?: RequestInit) => Promise<Response>;
};

/**
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<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`.
/**
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<HttpMethod, string>;

/**
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<
Expand Down
16 changes: 16 additions & 0 deletions source/utils/options.ts
@@ -0,0 +1,16 @@
import {kyOptionKeys} from '../core/constants.js';

export const findUnknownOptions = (
request: Request,
options: Record<string, unknown>,
): Record<string, unknown> => {
const unknownOptions: Record<string, unknown> = {};

for (const key in options) {
if (!(key in kyOptionKeys) && !(key in request)) {
unknownOptions[key] = options[key];
}
}

return unknownOptions;
};
3 changes: 2 additions & 1 deletion source/utils/timeout.ts
Expand Up @@ -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<Response> {
Expand All @@ -21,7 +22,7 @@ export default async function timeout(
}, options.timeout);

void options
.fetch(request)
.fetch(request, init)
.then(resolve)
.catch(reject)
.then(() => {
Expand Down
13 changes: 13 additions & 0 deletions test/fetch.ts
Expand Up @@ -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();
});

0 comments on commit e93bc6d

Please sign in to comment.