diff --git a/packages/http-router/src/Router.ts b/packages/http-router/src/Router.ts index ce8dd3e256dbf..4ebec2f5e02ff 100644 --- a/packages/http-router/src/Router.ts +++ b/packages/http-router/src/Router.ts @@ -5,10 +5,10 @@ import express from 'express'; import type { Context, HonoRequest, MiddlewareHandler } from 'hono'; import { Hono } from 'hono'; import type { StatusCode } from 'hono/utils/http-status'; -import qs from 'qs'; // Using qs specifically to keep express compatibility import type { ResponseSchema, TypedOptions } from './definition'; import { honoAdapterForExpress } from './middlewares/honoAdapterForExpress'; +import { parseQueryParams } from './parseQueryParams'; const logger = new Logger('HttpRouter'); @@ -186,7 +186,7 @@ export class Router< } protected parseQueryParams(request: HonoRequest) { - return qs.parse(request.raw.url.split('?')?.[1] || ''); + return parseQueryParams(request.raw.url.split('?')?.[1] || ''); } protected method( @@ -201,7 +201,14 @@ export class Router< this.innerRouter[method.toLowerCase() as Lowercase](`/${subpath}`.replace('//', '/'), ...middlewares, async (c) => { const { req, res } = c; - const queryParams = this.parseQueryParams(req); + let queryParams: Record; + try { + queryParams = this.parseQueryParams(req); + } catch (e) { + logger.warn({ msg: 'Error parsing query params for request', path: req.path, err: e }); + + return c.json({ success: false, error: 'Invalid query parameters' }, 400); + } if (options.query) { const validatorFn = options.query; diff --git a/packages/http-router/src/parseQueryParams.test.ts b/packages/http-router/src/parseQueryParams.test.ts new file mode 100644 index 0000000000000..9b7eaf2c5124a --- /dev/null +++ b/packages/http-router/src/parseQueryParams.test.ts @@ -0,0 +1,60 @@ +import { parseQueryParams } from './parseQueryParams'; + +describe('parseQueryParams', () => { + it('should parse simple query string', () => { + const result = parseQueryParams('foo=bar'); + expect(result).toEqual({ foo: 'bar' }); + }); + + it('should parse multiple query parameters', () => { + const result = parseQueryParams('foo=bar&baz=qux'); + expect(result).toEqual({ foo: 'bar', baz: 'qux' }); + }); + + it('should parse array parameters', () => { + const result = parseQueryParams('ids[]=1&ids[]=2&ids[]=3'); + expect(result).toEqual({ ids: ['1', '2', '3'] }); + }); + + it('should parse nested objects', () => { + const result = parseQueryParams('user[name]=john&user[age]=30'); + expect(result).toEqual({ user: { name: 'john', age: '30' } }); + }); + + it('should handle empty query string', () => { + const result = parseQueryParams(''); + expect(result).toEqual({}); + }); + + it('should decode URL encoded values', () => { + const result = parseQueryParams('name=John%20Doe'); + expect(result).toEqual({ name: 'John Doe' }); + }); + + it('should handle boolean-like values as strings', () => { + const result = parseQueryParams('active=true&disabled=false'); + expect(result).toEqual({ active: 'true', disabled: 'false' }); + }); + + it('should throw error when array limit is exceeded', () => { + const largeArray = Array(501) + .fill(0) + .map((_, i) => `ids[]=${i}`) + .join('&'); + expect(() => parseQueryParams(largeArray)).toThrow(); + }); + + it('should parse arrays within the limit', () => { + const array = Array(500) + .fill(0) + .map((_, i) => `ids[]=${i}`) + .join('&'); + const result = parseQueryParams(array); + expect(result.ids).toHaveLength(500); + }); + + it('should parse as array even without brackets', () => { + const result = parseQueryParams('ids=1&ids=2'); + expect(result.ids).toHaveLength(2); + }); +}); diff --git a/packages/http-router/src/parseQueryParams.ts b/packages/http-router/src/parseQueryParams.ts new file mode 100644 index 0000000000000..2bef5c58dede4 --- /dev/null +++ b/packages/http-router/src/parseQueryParams.ts @@ -0,0 +1,5 @@ +import qs from 'qs'; // Using qs specifically to keep express compatibility + +export function parseQueryParams(url: string) { + return qs.parse(url, { arrayLimit: 500, throwOnLimitExceeded: true }); +}