diff --git a/packages/void/src/runtime/validator.ts b/packages/void/src/runtime/validator.ts index f5e0a174c..6a1c34424 100644 --- a/packages/void/src/runtime/validator.ts +++ b/packages/void/src/runtime/validator.ts @@ -66,11 +66,12 @@ export async function runValidation( } if (validators.query) { - const url = new URL(c.req.url); - const raw: Record = {}; - for (const [key, value] of url.searchParams) { - raw[key] = value; - } + const raw = Object.fromEntries( + Object.entries(c.req.queries()).map(([key, values]) => [ + key, + values.length === 1 ? values[0] : values, + ]), + ); const result = await validateSlot(validators.query, raw); if (result.ok) { input.query = result.value; diff --git a/packages/void/test/unit/define-handler.test.ts b/packages/void/test/unit/define-handler.test.ts index fc6f63e5e..37f8b3450 100644 --- a/packages/void/test/unit/define-handler.test.ts +++ b/packages/void/test/unit/define-handler.test.ts @@ -257,6 +257,40 @@ describe('defineHandler.withValidator', () => { expect(await res.json()).toEqual({ query: { page: '2', limit: '10' } }); }); + it('preserves repeated query params as arrays', async () => { + const handler = defineHandler.withValidator({ + query: passSchema(), + })((_c, { query }) => { + return { query }; + }); + + const app = createValidatedApp('get', '/test', handler); + + const res = await app.request(new Request('http://localhost/test?page=2&tag=work&tag=urgent')); + + expect(res.status).toBe(200); + expect(await res.json()).toEqual({ + query: { + page: '2', + tag: ['work', 'urgent'], + }, + }); + }); + + it.each([ + ['__proto__=work', 'work'], + ['__proto__=work&__proto__=urgent', ['work', 'urgent']], + ])('preserves %s as an own query property', async (search, expected) => { + const handler = defineHandler.withValidator({ query: passSchema() })((_c, { query }) => ({ + query, + })); + const app = createValidatedApp('get', '/test', handler); + const res = await app.request(new Request(`http://localhost/test?${search}`)); + + expect(res.status).toBe(200); + expect(await res.json()).toEqual({ query: { ['__proto__']: expected } }); + }); + it('validates route params', async () => { const handler = defineHandler.withValidator({ params: passSchema(), diff --git a/packages/void/test/unit/validator-real-schemas.test.ts b/packages/void/test/unit/validator-real-schemas.test.ts index 5dc6399ec..8feb032c4 100644 --- a/packages/void/test/unit/validator-real-schemas.test.ts +++ b/packages/void/test/unit/validator-real-schemas.test.ts @@ -71,3 +71,20 @@ describe.each(libraries)('withValidator + $library', ({ CreateUser }) => { expect(json.issues[0].issues.length).toBeGreaterThanOrEqual(1); }); }); + +it('does not validate inherited array length as a query parameter', async () => { + const app = createPostApp( + defineHandler.withValidator({ query: z.object({ length: z.number() }) })( + (_c, { query }) => query, + ), + ); + const res = await app.request( + new Request('http://localhost/test?__proto__=work&__proto__=urgent', { method: 'POST' }), + ); + + expect(res.status).toBe(400); + const json = (await res.json()) as ValidationIssueBody; + expect(json.error).toBe('Validation failed'); + expect(json.issues[0].slot).toBe('query'); + expect(json.issues[0].issues[0].path).toBe('length'); +});