From d982f637ebab758715f7e68ff35a7c4844450023 Mon Sep 17 00:00:00 2001 From: Nanasi <71248588+spellsaif@users.noreply.github.com> Date: Wed, 12 Aug 2026 20:55:56 -0400 Subject: [PATCH 1/3] fix(url): strip trailing question mark correctly for optional params with regex quantifiers (#5209) * fix(url): strip trailing question mark correctly for optional params with regex quantifiers * fix(url): refine optional parameter detection to check trailing question mark Co-authored-by: Taku Amano --- src/utils/url.test.ts | 1 + src/utils/url.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/utils/url.test.ts b/src/utils/url.test.ts index 7274d5db73..2fcd70d727 100644 --- a/src/utils/url.test.ts +++ b/src/utils/url.test.ts @@ -253,6 +253,7 @@ describe('url', () => { '/api/:version/animal', '/api/:version/animal/:type', ]) + expect(checkOptionalParameter('/api/:id{[0-9]?}?')).toEqual(['/api', '/api/:id{[0-9]?}']) }) }) diff --git a/src/utils/url.ts b/src/utils/url.ts index ea92ff9355..1249364821 100644 --- a/src/utils/url.ts +++ b/src/utils/url.ts @@ -187,13 +187,13 @@ export const checkOptionalParameter = (path: string): string[] | null => { if (segment !== '' && !/\:/.test(segment)) { basePath += '/' + segment } else if (/\:/.test(segment)) { - if (/\?/.test(segment)) { + if (segment.charCodeAt(segment.length - 1) === 63) { if (results.length === 0 && basePath === '') { results.push('/') } else { results.push(basePath) } - const optionalSegment = segment.replace('?', '') + const optionalSegment = segment.slice(0, -1) basePath += '/' + optionalSegment results.push(basePath) } else { From a1e4ac7d46a3e627c143c4dd6dcf7bc59031bb9a Mon Sep 17 00:00:00 2001 From: Nanasi <71248588+spellsaif@users.noreply.github.com> Date: Wed, 12 Aug 2026 21:11:16 -0400 Subject: [PATCH 2/3] perf(cors): pre-join static array header options during initialization (#5210) * perf(cors): pre-join static array header options during initialization * fix(cors): normalize function return for allowMethods to preserve missing header semantics Co-authored-by: Taku Amano --- src/middleware/cors/index.test.ts | 11 +++++++++++ src/middleware/cors/index.ts | 31 +++++++++++++++++++------------ 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/middleware/cors/index.test.ts b/src/middleware/cors/index.test.ts index 9ea184c70b..ee9efdf2dc 100644 --- a/src/middleware/cors/index.test.ts +++ b/src/middleware/cors/index.test.ts @@ -382,6 +382,17 @@ describe('CORS by Middleware', () => { expect(res2.headers.get('Access-Control-Allow-Methods')).toBe('GET,HEAD') }) + it('Does not set allow methods when function returns an empty array', async () => { + for (const allowMethods of [() => [], async () => []]) { + const app = new Hono() + app.use(cors({ allowMethods })) + + const res = await app.request('http://localhost/', { method: 'OPTIONS' }) + + expect(res.headers.get('Access-Control-Allow-Methods')).toBeNull() + } + }) + it('Emits the wildcard, not the reflected origin, when credentials is true with wildcard origin', async () => { const res = await app.request('http://localhost/api10/abc', { headers: { diff --git a/src/middleware/cors/index.ts b/src/middleware/cors/index.ts index e4a45400ae..1ea2e012aa 100644 --- a/src/middleware/cors/index.ts +++ b/src/middleware/cors/index.ts @@ -69,6 +69,9 @@ export const cors = (options?: CORSOptions): MiddlewareHandler => { ...options, } satisfies CORSOptions + const exposeHeadersStr = opts.exposeHeaders?.length ? opts.exposeHeaders.join(',') : undefined + const allowHeadersStr = opts.allowHeaders?.length ? opts.allowHeaders.join(',') : undefined + const findAllowOrigin = ((optsOrigin) => { if (typeof optsOrigin === 'string') { if (optsOrigin === '*') { @@ -85,11 +88,12 @@ export const cors = (options?: CORSOptions): MiddlewareHandler => { const findAllowMethods = ((optsAllowMethods) => { if (typeof optsAllowMethods === 'function') { - return optsAllowMethods + return async (origin: string, c: Context) => (await optsAllowMethods(origin, c)).join(',') } else if (Array.isArray(optsAllowMethods)) { - return () => optsAllowMethods + const methodsStr = optsAllowMethods.join(',') + return () => methodsStr } else { - return () => [] + return () => '' } })(opts.allowMethods) @@ -107,8 +111,8 @@ export const cors = (options?: CORSOptions): MiddlewareHandler => { set('Access-Control-Allow-Credentials', 'true') } - if (opts.exposeHeaders?.length) { - set('Access-Control-Expose-Headers', opts.exposeHeaders.join(',')) + if (exposeHeadersStr) { + set('Access-Control-Expose-Headers', exposeHeadersStr) } if (c.req.method === 'OPTIONS') { @@ -121,19 +125,22 @@ export const cors = (options?: CORSOptions): MiddlewareHandler => { } const allowMethods = await findAllowMethods(c.req.header('origin') || '', c) - if (allowMethods.length) { - set('Access-Control-Allow-Methods', allowMethods.join(',')) + if (allowMethods) { + set('Access-Control-Allow-Methods', allowMethods) } - let headers = opts.allowHeaders - if (!headers?.length) { + let headersStr = allowHeadersStr + if (!headersStr) { const requestHeaders = c.req.header('Access-Control-Request-Headers') if (requestHeaders) { - headers = requestHeaders.split(',').map((h) => h.trim()) + headersStr = requestHeaders + .split(',') + .map((h) => h.trim()) + .join(',') } } - if (headers?.length) { - set('Access-Control-Allow-Headers', headers.join(',')) + if (headersStr) { + set('Access-Control-Allow-Headers', headersStr) c.res.headers.append('Vary', 'Access-Control-Request-Headers') } From 329b6f46865aad2fa1d0ac17fb53afc5a3216f1b Mon Sep 17 00:00:00 2001 From: Ersa Oktavian Ramadan <124137772+Ersaoktaviannn@users.noreply.github.com> Date: Thu, 13 Aug 2026 09:11:06 +0700 Subject: [PATCH 3/3] fix(client): send falsy JSON bodies (#5215) --- src/client/client.test.ts | 14 ++++++++++++++ src/client/client.ts | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/client/client.test.ts b/src/client/client.test.ts index defedcde4b..141e3c97ce 100644 --- a/src/client/client.test.ts +++ b/src/client/client.test.ts @@ -885,6 +885,20 @@ describe('Use custom fetch (app.request) method', () => { const res = await client.search.$get() expect(res.ok).toBe(true) }) + + it.each([false, 0, '', null])('Should send falsy JSON value: %j', async (json) => { + const app = new Hono().post( + '/json', + validator('json', (value) => value as boolean | number | string | null), + (c) => c.json(c.req.valid('json')) + ) + const client = hc('', { fetch: app.request }) + + const res = await client.json.$post({ json }) + + expect(res.status).toBe(200) + expect(await res.json()).toBe(json) + }) }) describe('Optional parameters in JSON response', () => { diff --git a/src/client/client.ts b/src/client/client.ts index 0a6144830b..1ce6d60a1f 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -78,7 +78,7 @@ class ClientRequestImpl { this.rBody = form } - if (args.json) { + if (args.json !== undefined) { this.rBody = JSON.stringify(args.json) this.cType = 'application/json' }