Skip to content

Commit add0703

Browse files
committed
Fix init hook URLSearchParams deletions
1 parent 8f28eac commit add0703

3 files changed

Lines changed: 87 additions & 8 deletions

File tree

source/core/Ky.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,14 @@ export class Ky {
436436

437437
if (hasSearchParameters(this.#options.searchParams)) {
438438
const url = new URL(this.request.url);
439+
const deleted = (this.#options.searchParams as any)?.[deletedParametersSymbol] as Set<string> | undefined;
440+
441+
if (deleted) {
442+
// Remove keys from the input URL first so later searchParams entries can intentionally re-add them.
443+
for (const key of deleted) {
444+
url.searchParams.delete(key);
445+
}
446+
}
439447

440448
if (typeof this.#options.searchParams === 'string') {
441449
const stringSearchParameters = this.#options.searchParams.replace(/^\?/, '');
@@ -463,13 +471,6 @@ export class Ky {
463471
}
464472
}
465473

466-
const deleted = (this.#options.searchParams as any)?.[deletedParametersSymbol] as Set<string> | undefined;
467-
if (deleted) {
468-
for (const key of deleted) {
469-
url.searchParams.delete(key);
470-
}
471-
}
472-
473474
// Recreate request with the updated URL. We already have all options in this.#options, including duplex.
474475
this.request = new globalThis.Request(url, this.#options as RequestInit);
475476
}

source/utils/merge.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,14 @@ const isPlainObject = (value: unknown): value is Record<string, unknown> => {
8888

8989
export const cloneShallow = <T>(value: T): T => {
9090
if (value instanceof URLSearchParams) {
91-
return new URLSearchParams(value) as T;
91+
const copy = new URLSearchParams(value) as URLSearchParams & {[deletedParametersSymbol]?: Set<string>};
92+
const deleted = (value as URLSearchParams & {[deletedParametersSymbol]?: Set<string>})[deletedParametersSymbol];
93+
if (deleted) {
94+
// Preserve internal deletion markers so init-hook cloning does not resurrect params removed during option merging.
95+
copy[deletedParametersSymbol] = new Set(deleted);
96+
}
97+
98+
return copy as T;
9299
}
93100

94101
if (value instanceof globalThis.Headers) {

test/main.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1118,6 +1118,77 @@ test('init hook deletion over merged defaults and input URL', async t => {
11181118
t.false(url.searchParams.has('foo'));
11191119
});
11201120

1121+
test('init hook preserves merged URLSearchParams deletions', async t => {
1122+
const server = await createHttpTestServer(t);
1123+
1124+
server.get('/', (request, response) => {
1125+
response.end(request.url);
1126+
});
1127+
1128+
const api = ky.create({searchParams: new URLSearchParams({foo: '1'})}).extend({
1129+
searchParams: {foo: undefined},
1130+
hooks: {
1131+
init: [
1132+
() => {}, // eslint-disable-line @typescript-eslint/no-empty-function
1133+
],
1134+
},
1135+
});
1136+
1137+
const response = await api.get(`${server.url}?foo=from-url&bar=2`);
1138+
const url = new URL(await response.text(), server.url);
1139+
1140+
t.false(url.searchParams.has('foo'));
1141+
t.is(url.searchParams.get('bar'), '2');
1142+
});
1143+
1144+
test('init hook preserves merged plain object deletions', async t => {
1145+
const server = await createHttpTestServer(t);
1146+
1147+
server.get('/', (request, response) => {
1148+
response.end(request.url);
1149+
});
1150+
1151+
const api = ky.create({searchParams: {foo: '1'}}).extend({
1152+
searchParams: {foo: undefined},
1153+
hooks: {
1154+
init: [
1155+
() => {}, // eslint-disable-line @typescript-eslint/no-empty-function
1156+
],
1157+
},
1158+
});
1159+
1160+
const response = await api.get(`${server.url}?foo=from-url&bar=2`);
1161+
const url = new URL(await response.text(), server.url);
1162+
1163+
t.false(url.searchParams.has('foo'));
1164+
t.is(url.searchParams.get('bar'), '2');
1165+
});
1166+
1167+
test('init hook can re-add deleted URLSearchParams keys in place', async t => {
1168+
const server = await createHttpTestServer(t);
1169+
1170+
server.get('/', (request, response) => {
1171+
response.end(request.url);
1172+
});
1173+
1174+
const api = ky.create({searchParams: new URLSearchParams({foo: '1'})}).extend({
1175+
searchParams: {foo: undefined},
1176+
hooks: {
1177+
init: [
1178+
options => {
1179+
(options.searchParams as URLSearchParams).append('foo', '2');
1180+
},
1181+
],
1182+
},
1183+
});
1184+
1185+
const response = await api.get(`${server.url}?foo=from-url&bar=2`);
1186+
const url = new URL(await response.text(), server.url);
1187+
1188+
t.deepEqual(url.searchParams.getAll('foo'), ['2']);
1189+
t.is(url.searchParams.get('bar'), '2');
1190+
});
1191+
11211192
test('re-adding a key after an earlier deletion across merge layers', async t => {
11221193
const server = await createHttpTestServer(t);
11231194

0 commit comments

Comments
 (0)