Skip to content

Commit 06375ef

Browse files
Fix extend() dropping numeric retry limit when merging with an object (#867)
1 parent 6edddd9 commit 06375ef

2 files changed

Lines changed: 47 additions & 2 deletions

File tree

source/utils/merge.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ const appendSearchParameters = (target: any, source: any): URLSearchParams => {
204204
};
205205

206206
// TODO: Make this strongly-typed (no `any`).
207-
export const deepMerge = <T>(...sources: Array<Partial<T> | undefined>): T => {
207+
const deepMergeInternal = <T>(isRoot: boolean, ...sources: Array<Partial<T> | undefined>): T => {
208208
let returnValue: any = {};
209209
let headers: KyHeadersInit = {};
210210
let hooks = {};
@@ -264,8 +264,17 @@ export const deepMerge = <T>(...sources: Array<Partial<T> | undefined>): T => {
264264
continue;
265265
}
266266

267+
// `retry` accepts a number as shorthand for `{limit: number}`. Expand it before
268+
// merging so extending a numeric `retry` with an object keeps the limit instead
269+
// of dropping it (e.g. `ky.create({retry: 3}).extend({retry: {methods: ['get']}})`).
270+
// Scoped to the root options level so it never rewrites nested user data that
271+
// happens to contain a `retry` key (e.g. a `json` request body).
272+
if (isRoot && key === 'retry' && isObject(value) && !isReplace && typeof returnValue[key] === 'number') {
273+
returnValue = {...returnValue, [key]: {limit: returnValue[key]}};
274+
}
275+
267276
if (isObject(value) && !isReplace && key in returnValue) {
268-
value = deepMerge(returnValue[key], value);
277+
value = deepMergeInternal(false, returnValue[key], value);
269278
}
270279

271280
returnValue = {...returnValue, [key]: value};
@@ -310,3 +319,6 @@ export const deepMerge = <T>(...sources: Array<Partial<T> | undefined>): T => {
310319

311320
return returnValue;
312321
};
322+
323+
export const deepMerge = <T>(...sources: Array<Partial<T> | undefined>): T =>
324+
deepMergeInternal<T>(true, ...sources);

test/retry.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,39 @@ test('retry - can provide retry as number', async t => {
468468
t.is(requestCount, 5);
469469
});
470470

471+
test('retry - extending a numeric `retry` with an object keeps the limit', async t => {
472+
let requestCount = 0;
473+
474+
const server = await createHttpTestServer(t);
475+
server.get('/', async (_request, response) => {
476+
requestCount++;
477+
response.sendStatus(408);
478+
});
479+
480+
// `retry: 3` is shorthand for `{limit: 3}`. Extending it with an object
481+
// should preserve that limit instead of falling back to the default.
482+
const extended = ky.create({retry: 3}).extend({retry: {methods: ['get']}});
483+
484+
await t.throwsAsync(extended(server.url).text(), {
485+
message: /Request Timeout/,
486+
});
487+
t.is(requestCount, 4);
488+
});
489+
490+
test('retry - shorthand expansion does not rewrite nested user data with a `retry` key', async t => {
491+
const server = await createHttpTestServer(t);
492+
server.post('/', (request, response) => {
493+
response.json({body: request.body});
494+
});
495+
496+
// A `retry` key inside the `json` body is user data, not the `retry` option,
497+
// so the number-to-`{limit}` shorthand must not touch it.
498+
const client = ky.create({json: {retry: 3}}).extend({json: {retry: {foo: 'bar'}}});
499+
500+
const {body} = await client.post(server.url).json<{body: {retry: unknown}}>();
501+
t.deepEqual(body.retry, {foo: 'bar'});
502+
});
503+
471504
test('doesn\'t retry on 413 with empty statusCodes and methods', async t => {
472505
let requestCount = 0;
473506

0 commit comments

Comments
 (0)