Skip to content

Commit 0017053

Browse files
Allow explicitly unsetting hooks on .extend() (#599)
* feat: allow explicitly unsetting hooks on `.extend()` The implementation here is pretty simple: just give hook merging some custom logic so it either merges OR resets to an empty array. Matches what was discussed in the related ticket. Documentation has been updated to describe this behavior as well. The dedicated test is just a modified copy of the existing `ky.extend()` one. Admittedly, I did modify a couple of other tests to cover the edge cases instead of building brand new tests. No reason other than noticing they were already close to what I needed and I'm being lazy. Closes #408 * chore: remove unused import
1 parent 786a9de commit 0017053

File tree

4 files changed

+93
-4
lines changed

4 files changed

+93
-4
lines changed

readme.md

+13-1
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,8 @@ You can pass headers as a `Headers` instance or a plain object.
487487
You can remove a header with `.extend()` by passing the header with an `undefined` value.
488488
Passing `undefined` as a string removes the header only if it comes from a `Headers` instance.
489489

490+
Similarly, you can remove existing `hooks` entries by extending the hook with an explicit `undefined`.
491+
490492
```js
491493
import ky from 'ky';
492494

@@ -496,16 +498,26 @@ const original = ky.create({
496498
headers: {
497499
rainbow: 'rainbow',
498500
unicorn: 'unicorn'
499-
}
501+
},
502+
hooks: {
503+
beforeRequest: [ () => console.log('before 1') ],
504+
afterResponse: [ () => console.log('after 1') ],
505+
},
500506
});
501507

502508
const extended = original.extend({
503509
headers: {
504510
rainbow: undefined
511+
},
512+
hooks: {
513+
beforeRequest: undefined,
514+
afterResponse: [ () => console.log('after 2') ],
505515
}
506516
});
507517

508518
const response = await extended(url).json();
519+
//=> after 1
520+
//=> after 2
509521

510522
console.log('rainbow' in response);
511523
//=> false

source/core/Ky.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import {HTTPError} from '../errors/HTTPError.js';
22
import {TimeoutError} from '../errors/TimeoutError.js';
3-
import type {Hooks} from '../types/hooks.js';
43
import type {
54
Input,
65
InternalOptions,
@@ -9,7 +8,7 @@ import type {
98
SearchParamsInit,
109
} from '../types/options.js';
1110
import {type ResponsePromise} from '../types/ResponsePromise.js';
12-
import {deepMerge, mergeHeaders} from '../utils/merge.js';
11+
import {mergeHeaders, mergeHooks} from '../utils/merge.js';
1312
import {normalizeRequestMethod, normalizeRetryOptions} from '../utils/normalize.js';
1413
import timeout, {type TimeoutOptions} from '../utils/timeout.js';
1514
import delay from '../utils/delay.js';
@@ -133,7 +132,7 @@ export class Ky {
133132
...(credentials && {credentials}), // For exactOptionalPropertyTypes
134133
...options,
135134
headers: mergeHeaders((this._input as Request).headers, options.headers),
136-
hooks: deepMerge<Required<Hooks>>(
135+
hooks: mergeHooks(
137136
{
138137
beforeRequest: [],
139138
beforeRetry: [],

source/utils/merge.ts

+22
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type {KyHeadersInit, Options} from '../types/options.js';
2+
import type {Hooks} from '../types/hooks.js';
23
import {isObject} from './is.js';
34

45
export const validateAndMerge = (...sources: Array<Partial<Options> | undefined>): Partial<Options> => {
@@ -27,10 +28,26 @@ export const mergeHeaders = (source1: KyHeadersInit = {}, source2: KyHeadersInit
2728
return result;
2829
};
2930

31+
function newHookValue<K extends keyof Hooks>(original: Hooks, incoming: Hooks, property: K): Required<Hooks>[K] {
32+
return (Object.hasOwn(incoming, property) && incoming[property] === undefined)
33+
? []
34+
: deepMerge<Required<Hooks>[K]>(original[property] ?? [], incoming[property] ?? []);
35+
}
36+
37+
export const mergeHooks = (original: Hooks = {}, incoming: Hooks = {}): Required<Hooks> => (
38+
{
39+
beforeRequest: newHookValue(original, incoming, 'beforeRequest'),
40+
beforeRetry: newHookValue(original, incoming, 'beforeRetry'),
41+
afterResponse: newHookValue(original, incoming, 'afterResponse'),
42+
beforeError: newHookValue(original, incoming, 'beforeError'),
43+
}
44+
);
45+
3046
// TODO: Make this strongly-typed (no `any`).
3147
export const deepMerge = <T>(...sources: Array<Partial<T> | undefined>): T => {
3248
let returnValue: any = {};
3349
let headers = {};
50+
let hooks = {};
3451

3552
for (const source of sources) {
3653
if (Array.isArray(source)) {
@@ -48,6 +65,11 @@ export const deepMerge = <T>(...sources: Array<Partial<T> | undefined>): T => {
4865
returnValue = {...returnValue, [key]: value};
4966
}
5067

68+
if (isObject((source as any).hooks)) {
69+
hooks = mergeHooks(hooks, (source as any).hooks);
70+
returnValue.hooks = hooks;
71+
}
72+
5173
if (isObject((source as any).headers)) {
5274
headers = mergeHeaders(headers, (source as any).headers);
5375
returnValue.headers = headers;

test/main.ts

+56
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,7 @@ test('ky.create() with deep array', async t => {
500500

501501
let isOriginBeforeRequestTrigged = false;
502502
let isExtendBeforeRequestTrigged = false;
503+
let isExtendAfterResponseTrigged = false;
503504

504505
const extended = ky.create({
505506
hooks: {
@@ -518,11 +519,17 @@ test('ky.create() with deep array', async t => {
518519
isExtendBeforeRequestTrigged = true;
519520
},
520521
],
522+
afterResponse: [
523+
() => {
524+
isExtendAfterResponseTrigged = true;
525+
},
526+
],
521527
},
522528
});
523529

524530
t.is(isOriginBeforeRequestTrigged, true);
525531
t.is(isExtendBeforeRequestTrigged, true);
532+
t.is(isExtendAfterResponseTrigged, true);
526533

527534
const {ok} = await extended.head(server.url);
528535
t.true(ok);
@@ -549,6 +556,7 @@ const extendHooksMacro = test.macro<[{useFunction: boolean}]>(async (t, {useFunc
549556
});
550557

551558
let isOriginBeforeRequestTrigged = false;
559+
let isOriginAfterResponseTrigged = false;
552560
let isExtendBeforeRequestTrigged = false;
553561

554562
const intermediateOptions = {
@@ -558,6 +566,11 @@ const extendHooksMacro = test.macro<[{useFunction: boolean}]>(async (t, {useFunc
558566
isOriginBeforeRequestTrigged = true;
559567
},
560568
],
569+
afterResponse: [
570+
() => {
571+
isOriginAfterResponseTrigged = true;
572+
},
573+
],
561574
},
562575
};
563576
const extendedOptions = {
@@ -577,6 +590,7 @@ const extendHooksMacro = test.macro<[{useFunction: boolean}]>(async (t, {useFunc
577590
await extended(server.url);
578591

579592
t.is(isOriginBeforeRequestTrigged, true);
593+
t.is(isOriginAfterResponseTrigged, true);
580594
t.is(isExtendBeforeRequestTrigged, true);
581595

582596
const {ok} = await extended.head(server.url);
@@ -639,6 +653,48 @@ test('ky.extend() with function retains parent defaults when not specified', asy
639653
await server.close();
640654
});
641655

656+
test('ky.extend() can remove hooks', async t => {
657+
const server = await createHttpTestServer();
658+
server.get('/', (_request, response) => {
659+
response.end();
660+
});
661+
662+
let isOriginalBeforeRequestTrigged = false;
663+
let isOriginalAfterResponseTrigged = false;
664+
665+
const extended = ky
666+
.extend({
667+
hooks: {
668+
beforeRequest: [
669+
() => {
670+
isOriginalBeforeRequestTrigged = true;
671+
},
672+
],
673+
afterResponse: [
674+
() => {
675+
isOriginalAfterResponseTrigged = true;
676+
},
677+
],
678+
},
679+
})
680+
.extend({
681+
hooks: {
682+
beforeRequest: undefined,
683+
afterResponse: [],
684+
},
685+
});
686+
687+
await extended(server.url);
688+
689+
t.is(isOriginalBeforeRequestTrigged, false);
690+
t.is(isOriginalAfterResponseTrigged, true);
691+
692+
const {ok} = await extended.head(server.url);
693+
t.true(ok);
694+
695+
await server.close();
696+
});
697+
642698
test('throws DOMException/Error with name AbortError when aborted by user', async t => {
643699
const server = await createHttpTestServer();
644700
// eslint-disable-next-line @typescript-eslint/no-empty-function

0 commit comments

Comments
 (0)