set res() setter loses Set-Cookie headers when middleware sets cookies before next() and handler returns a raw Response
triage
### 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):
```js
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`:
```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:
```ts
// 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.
5 条评论