From 2bac91d19d79d64b6f07c78b56cf8ba64394005f Mon Sep 17 00:00:00 2001 From: mdong Date: Sun, 6 Sep 2026 14:40:09 +0800 Subject: [PATCH 1/2] fix(auth): handle immutable headers for public routes --- packages/void/src/runtime/better-auth-shared.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/void/src/runtime/better-auth-shared.ts b/packages/void/src/runtime/better-auth-shared.ts index 011bca4ba..acb61ed7e 100644 --- a/packages/void/src/runtime/better-auth-shared.ts +++ b/packages/void/src/runtime/better-auth-shared.ts @@ -217,17 +217,15 @@ function resolveSocialProviders( return Object.keys(providers).length > 0 ? providers : undefined; } -function appendAuthHeaders(target: Headers, source?: Headers): void { +function appendAuthHeaders(c: Context, source?: Headers): void { if (!source) { return; } source.forEach((value, key) => { - if (key.toLowerCase() === 'set-cookie') { - target.append(key, value); - return; - } - target.set(key, value); + c.header(key, value, { + append: key.toLowerCase() === 'set-cookie', + }); }); } @@ -397,7 +395,7 @@ export function createVoidAuthRuntime( await next(); }); - appendAuthHeaders(c.res.headers, result.headers); + appendAuthHeaders(c, result.headers); } finally { await dispose(); } From b02c49e6fd0627c4fc1d4e86567cfaf345167aa7 Mon Sep 17 00:00:00 2001 From: mdong Date: Sun, 6 Sep 2026 14:47:41 +0800 Subject: [PATCH 2/2] tests(auth): add tests --- .../void/test/unit/auth-middleware.test.ts | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 packages/void/test/unit/auth-middleware.test.ts diff --git a/packages/void/test/unit/auth-middleware.test.ts b/packages/void/test/unit/auth-middleware.test.ts new file mode 100644 index 000000000..6a2a56235 --- /dev/null +++ b/packages/void/test/unit/auth-middleware.test.ts @@ -0,0 +1,98 @@ +import { Hono } from 'hono'; +import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'; + +const mocks = vi.hoisted(() => ({ + betterAuth: vi.fn(), + getSession: vi.fn(), +})); + +vi.mock('better-auth', () => ({ + betterAuth: mocks.betterAuth, +})); + +import { + createVoidAuthRuntime, + type VoidAuthRuntimeOptions, +} from '../../src/runtime/better-auth-shared.ts'; + +const authRuntimeOptions: VoidAuthRuntimeOptions = { + dialect: 'sqlite', + providers: null, +}; + +const runtime = createVoidAuthRuntime( + 'sqlite', + async () => { + throw new Error('Unexpected database resolution.'); + }, + async () => {}, +); + +function createApp(sessionResult: { response: null; headers: Headers }): Hono { + mocks.getSession.mockResolvedValue(sessionResult); + mocks.betterAuth.mockReturnValue({ + api: { getSession: mocks.getSession }, + }); + + const app = new Hono(); + app.use(async (c, next) => { + await runtime.runVoidAuthMiddleware(c, next, authRuntimeOptions, {} as never); + }); + return app; +} + +beforeEach(() => { + mocks.betterAuth.mockReset(); + mocks.getSession.mockReset(); +}); + +describe('runVoidAuthMiddleware response headers', () => { + it('adds auth headers to an immutable response without changing its body or status', async () => { + const authHeaders = new Headers(); + authHeaders.set('x-auth-refresh', 'refreshed'); + authHeaders.append('set-cookie', 'session=one; Path=/; HttpOnly'); + authHeaders.append('set-cookie', 'csrf=two; Path=/'); + const app = createApp({ response: null, headers: authHeaders }); + + app.get('/public', () => fetch('data:text/plain,public%20content')); + + const response = await app.request('http://localhost/public', undefined, {}); + + expect(response.status).toBe(200); + expect(response.headers.get('x-auth-refresh')).toBe('refreshed'); + expect(response.headers.getSetCookie()).toEqual([ + 'session=one; Path=/; HttpOnly', + 'csrf=two; Path=/', + ]); + await expect(response.text()).resolves.toBe('public content'); + }); + + it('appends auth cookies while replacing other response headers', async () => { + const authHeaders = new Headers(); + authHeaders.set('cache-control', 'private, no-store'); + authHeaders.append('set-cookie', 'session=auth; Path=/; HttpOnly'); + const app = createApp({ response: null, headers: authHeaders }); + + app.get( + '/response', + () => + new Response('ok', { + status: 202, + headers: { + 'cache-control': 'public, max-age=60', + 'set-cookie': 'route=existing; Path=/', + }, + }), + ); + + const response = await app.request('http://localhost/response', undefined, {}); + + expect(response.status).toBe(202); + expect(response.headers.get('cache-control')).toBe('private, no-store'); + expect(response.headers.getSetCookie()).toEqual([ + 'route=existing; Path=/', + 'session=auth; Path=/; HttpOnly', + ]); + await expect(response.text()).resolves.toBe('ok'); + }); +});