diff --git a/src/url.ts b/src/url.ts index 425c7e9..4738ac9 100644 --- a/src/url.ts +++ b/src/url.ts @@ -31,13 +31,13 @@ function stringify( return queryString; } - const encodedKey = prefix - ? encodeURIComponent(`${prefix}[${key}]`) - : encodeURIComponent(key); + // Build the unencoded key path first + const keyPath = prefix ? `${prefix}[${key}]` : key; // Check if value is an object and we haven't exceeded max depth if (typeof value === "object" && value !== null && depth < maxDepth) { - const nestedQuery = stringifyRecursive(value, encodedKey, depth + 1); + // Pass unencoded keyPath as prefix to prevent double encoding in recursive calls + const nestedQuery = stringifyRecursive(value, keyPath, depth + 1); // Only append if nestedQuery has content if (!nestedQuery) { @@ -48,6 +48,8 @@ function stringify( ? `${queryString}${separator}${nestedQuery}` : nestedQuery; } else { + // Encode only once at the leaf level + const encodedKey = encodeURIComponent(keyPath); const encodedValue = typeof value === "object" && value !== null ? "" diff --git a/tests/url.spec.ts b/tests/url.spec.ts index 7561530..1040188 100644 --- a/tests/url.spec.ts +++ b/tests/url.spec.ts @@ -195,4 +195,27 @@ describe("DynamicURL", () => { expect(result).toBe("https://example.com?citizen=robespierre"); expect(result).not.toContain("emptyNested"); }); + + test("should not double-encode nested objects at 3+ levels deep", () => { + const url = new DynamicURL("https://example.com"); + url.setQueryParams({ + level1: { + level2: { + level3: "value", + }, + }, + }); + + const result = url.resolve(); + // Should have single encoding: level1[level2][level3] + expect(result).toBe( + "https://example.com?level1%5Blevel2%5D%5Blevel3%5D=value" + ); + + // Verify decoded key is correct + const queryString = result.split("?")[1]; + const params = new URLSearchParams(queryString); + const keys = Array.from(params.keys()); + expect(keys[0]).toBe("level1[level2][level3]"); + }); });