ITADN

set res() setter loses Set-Cookie headers when middleware sets cookies before next() and handler returns a raw Response

#4992Opentechzealot 创建于 2026-06-02
triage
T
techzealotcommented
### 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 条评论