The mistake we kept making
auth.handler(request) takes a Fetch Request and returns a Fetch Response. The instinctive Hono pattern is to wrap it:
- Multi-valued Set-Cookie headers collapse.
Headers.forEachyields everySet-Cookieseparately,c.header(k, v)is last-write-wins, only the last cookie survives. BetterAuth sets state + csrf + session cookies on/sign-in/socialand the OAuth callback; losing any of them breaks OAuth state mismatch, silent session loss, or phantom CSRF errors. - Hono’s context headers are ignored when the handler returns its own Response. All those
c.header()calls are dead code. The newResponse(body, { status })ships with exactly zero headers from the context. CORS headers set by upstream middleware never reach the outgoing response. Cross-origingetSession()calls silently fail in the browser even though the preflight succeeded and the body is correct.
What works
- Pass
c.req.rawdirectly. Never reconstruct the request — the original has the right method, headers, body stream, and URL. - Enumerate cookies via
Headers.getSetCookie()(a native Fetch spec API, not a Node extension). It returns astring[]of everySet-Cookievalue, and thenheaders.append("set-cookie", v)preserves them. - Apply CORS explicitly on the Headers bag. Hono context headers only apply to responses Hono constructs for you. If you build a
new Response(), you own the headers.
Lesson
Treatauth.handler as a sealed black box. Pass the request in, take the response out, mutate only via new Headers() copy. Any time you find yourself reaching for c.header() inside a BetterAuth route handler, you’re about to write a bug.