What version of Hono are you using?
4.12.18
What runtime/platform is your app running on? (with version if possible)
Node.js v24.1.0
What steps can reproduce the bug?
When a middleware sets a Set-Cookie header via c.header() before await next(), and the downstream handler returns a raw Response object with its own Set-Cookie headers, one side's cookies are always lost — they are never merged correctly.
Minimal reproduction (no third-party dependencies):
import { Hono } from 'hono'
const app = new Hono()
// Middleware: access c.res (e.g. CORS does this), then set a cookie
app.use(async (c, next) => {
c.res.headers.set('X-Something', 'true') // triggers #res creation
c.header('Set-Cookie', 'mw_cookie=hello; Path=/', { append: true })
await next()
})
// Handler: returns a raw Response with its own Set-Cookie
app.get('/test', (c) => {
const res = new Response('ok')
res.headers.append('set-cookie', 'handler_cookie=world; Path=/; HttpOnly')
return res
})
const res = await app.request('/test')
console.log(res.headers.getSetCookie())
// Actual: ['mw_cookie=hello; Path=/']
// Expected: ['mw_cookie=hello; Path=/', 'handler_cookie=world; Path=/; HttpOnly']
What is the expected behavior?
Both Set-Cookie headers should be present in the final response:
mw_cookie=hello (set by middleware via c.header())
handler_cookie=world (set by the handler's raw Response)
What do you see instead?
Only one side's cookies survive, depending on whether c.res was accessed before c.header():
| Scenario |
#res created before c.header()? |
Lost cookies |
A: No middleware touches c.res early |
No |
c.header() cookies lost (merge skipped entirely) |
B: Some middleware accesses c.res early (e.g. CORS) |
Yes |
Handler's cookies overwritten |
Additional information
Root cause
The bug is in the set res() setter in src/context.ts:
set res(_res) {
if (this.#res && _res) { // ← Scenario A: #res is falsy → entire merge is skipped
_res = createResponseInstance(_res.body, _res)
for (const [k, v] of this.#res.headers.entries()) {
if (k === 'set-cookie') {
const cookies = this.#res.headers.getSetCookie()
_res.headers.delete('set-cookie') // ← Scenario B: deletes handler's cookies
for (const cookie of cookies) {
_res.headers.append('set-cookie', cookie) // ← only re-appends context's cookies
}
}
// ...
}
}
this.#res = _res
}
Scenario A — this.#res is undefined (no middleware accessed c.res before c.header()):
c.header() writes to #preparedHeaders, but the if (this.#res && _res) guard is false
#preparedHeaders are never merged into the handler's Response → middleware cookies lost
Scenario B — this.#res exists (e.g. CORS middleware called c.res.headers.set() earlier):
_res.headers.delete('set-cookie') wipes ALL cookies from the handler's Response
- Only the context's cookies are re-appended → handler cookies lost
Suggested fix
The merge logic should preserve cookies from both sides:
// this only fix Scenario B
if (k === 'set-cookie') {
// Collect cookies from both sources before mutating
const handlerCookies = _res.headers.getSetCookie()
const contextCookies = this.#res.headers.getSetCookie()
_res.headers.delete('set-cookie')
for (const cookie of handlerCookies) {
_res.headers.append('set-cookie', cookie)
}
for (const cookie of contextCookies) {
_res.headers.append('set-cookie', cookie)
}
}
Additionally, Scenario A needs #preparedHeaders to be merged even when #res hasn't been created yet.
Real-world impact
This bug surfaces in common middleware combinations. For example, in our project:
CORS middleware (accesses c.res.headers)
→ languageDetector({ caches: true }) // calls setCookie() → c.header("Set-Cookie", ...)
→ better-auth handler // returns raw Response with session Set-Cookie headers
With caches: true, the languageDetector sets a language cookie via c.header() before next(). When better-auth's handler returns a raw Response with session cookies, the set-cookie merge in set res() causes cookies from one side to be dropped.
We currently work around this by setting caches: false, but this disables a legitimate feature. The issue is not specific to languageDetector or better-auth — any middleware that calls c.header('Set-Cookie', ...) before next() combined with any handler that returns a raw Response with Set-Cookie headers will trigger this bug.
What version of Hono are you using?
4.12.18
What runtime/platform is your app running on? (with version if possible)
Node.js v24.1.0
What steps can reproduce the bug?
When a middleware sets a
Set-Cookieheader viac.header()beforeawait next(), and the downstream handler returns a rawResponseobject with its ownSet-Cookieheaders, one side's cookies are always lost — they are never merged correctly.Minimal reproduction (no third-party dependencies):
What is the expected behavior?
Both
Set-Cookieheaders should be present in the final response:mw_cookie=hello(set by middleware viac.header())handler_cookie=world(set by the handler's rawResponse)What do you see instead?
Only one side's cookies survive, depending on whether
c.reswas accessed beforec.header():#rescreated beforec.header()?c.researlyc.header()cookies lost (merge skipped entirely)c.researly (e.g. CORS)Additional information
Root cause
The bug is in the
set res()setter insrc/context.ts:Scenario A —
this.#resisundefined(no middleware accessedc.resbeforec.header()):c.header()writes to#preparedHeaders, but theif (this.#res && _res)guard isfalse#preparedHeadersare never merged into the handler's Response → middleware cookies lostScenario B —
this.#resexists (e.g. CORS middleware calledc.res.headers.set()earlier):_res.headers.delete('set-cookie')wipes ALL cookies from the handler's ResponseSuggested fix
The merge logic should preserve cookies from both sides:
Additionally, Scenario A needs
#preparedHeadersto be merged even when#reshasn't been created yet.Real-world impact
This bug surfaces in common middleware combinations. For example, in our project:
With
caches: true, thelanguageDetectorsets a language cookie viac.header()beforenext(). Whenbetter-auth's handler returns a rawResponsewith session cookies, theset-cookiemerge inset res()causes cookies from one side to be dropped.We currently work around this by setting
caches: false, but this disables a legitimate feature. The issue is not specific tolanguageDetectororbetter-auth— any middleware that callsc.header('Set-Cookie', ...)beforenext()combined with any handler that returns a rawResponsewithSet-Cookieheaders will trigger this bug.