From b7ea609d1b534846cdc5bcdd1eaef149167e77e7 Mon Sep 17 00:00:00 2001 From: atharvasingh7007 Date: Thu, 16 Apr 2026 12:52:04 +0530 Subject: [PATCH] fix: isolate tuple searchParams in init hooks --- source/core/Ky.ts | 10 +++++++++- test/hooks.ts | 30 ++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/source/core/Ky.ts b/source/core/Ky.ts index 5e8c8f57..2ede2a7a 100644 --- a/source/core/Ky.ts +++ b/source/core/Ky.ts @@ -84,6 +84,14 @@ const isRequestInstance = (value: unknown): value is Request => const isResponseInstance = (value: unknown): value is Response => value instanceof globalThis.Response || objectToString.call(value) === '[object Response]'; +const cloneSearchParametersForInitHook = (searchParameters: SearchParamsOption | undefined): SearchParamsOption | undefined => { + if (Array.isArray(searchParameters)) { + return searchParameters.map(parameter => [...parameter]) as SearchParamsOption; + } + + return cloneShallow(searchParameters) as SearchParamsOption | undefined; +}; + // Shallow-clone mutable option properties so init hook mutations don't leak across requests. function cloneInitHookOptions(options: Options): Options { const clonedOptions: Options = { @@ -91,7 +99,7 @@ function cloneInitHookOptions(options: Options): Options { json: cloneShallow(options.json), context: cloneShallow(options.context)!, headers: cloneShallow(options.headers)!, - searchParams: cloneShallow(options.searchParams) as SearchParamsOption | undefined, + searchParams: cloneSearchParametersForInitHook(options.searchParams), }; if (options.retry !== undefined) { diff --git a/test/hooks.ts b/test/hooks.ts index 5059c096..66e6f444 100644 --- a/test/hooks.ts +++ b/test/hooks.ts @@ -4900,6 +4900,36 @@ test('init hook in-place mutations do not leak across requests', async t => { t.deepEqual(seenRequestIdentifiers, ['1', '2']); }); +test('init hook tuple searchParams mutations do not leak across requests', async t => { + let requestIdentifier = 0; + const seenInitialValues: string[] = []; + const seenRequestIdentifiers: string[] = []; + + const api = ky.extend({ + searchParams: [['requestId', 'seed']], + hooks: { + init: [ + options => { + const searchParameters = options.searchParams as string[][]; + seenInitialValues.push(searchParameters[0]![1]!); + searchParameters[0]![1] = String(++requestIdentifier); + }, + ], + }, + }); + + const fetch: typeof globalThis.fetch = async request => { + seenRequestIdentifiers.push(new URL(request.url).searchParams.get('requestId')!); + return new Response('ok'); + }; + + await api.get('https://example.com', {fetch}); + await api.get('https://example.com', {fetch}); + + t.deepEqual(seenInitialValues, ['seed', 'seed']); + t.deepEqual(seenRequestIdentifiers, ['1', '2']); +}); + test('init hook in-place retry mutations do not leak across requests', async t => { const seenLimits: number[] = [];