fix(context): preserve prepared Set-Cookie headers when assigning raw Response#4994
fix(context): preserve prepared Set-Cookie headers when assigning raw Response#4994xu91102 wants to merge 1 commit into
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4994 +/- ##
=======================================
Coverage 78.98% 78.98%
=======================================
Files 154 154
Lines 10568 10571 +3
Branches 2212 2214 +2
=======================================
+ Hits 8347 8350 +3
Misses 2221 2221 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
Thanks for the fix! I tested it locally (hono 4.12.18, Reproduction scriptimport { Hono } from 'hono'
// ─── Scenario B: #res created before c.header() ───
{
const app = new Hono()
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()
})
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('[Scenario B]', res.headers.getSetCookie())
}
// ─── Scenario A: only #preparedHeaders, #res never created ───
{
const app = new Hono()
app.use(async (c, next) => {
c.header('Set-Cookie', 'mw_cookie=hello; Path=/', { append: true })
await next()
})
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('[Scenario A]', res.headers.getSetCookie())
}Output with the official fix appliedRoot causeThe if (k === 'set-cookie') {
const cookies = headers.getSetCookie() // ← only context's cookies
_res.headers.delete('set-cookie') // ← handler's cookies gone
for (const cookie of cookies) {
_res.headers.append('set-cookie', cookie) // ← only context's re-appended
}
}Suggested correct fixScenario B — save handler's cookies before if (k === 'set-cookie') {
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)
}Scenario A — use if (k === 'set-cookie') {
_res.headers.append('set-cookie', v)
}Full corrected setter: set res(_res: Response | undefined) {
if (this.#res && _res) {
_res = createResponseInstance(_res.body, _res)
for (const [k, v] of this.#res.headers.entries()) {
if (k === 'content-type') continue
if (k === 'set-cookie') {
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)
} else {
_res.headers.set(k, v)
}
}
} else if (this.#preparedHeaders && _res) {
_res = createResponseInstance(_res.body, _res)
for (const [k, v] of this.#preparedHeaders.entries()) {
if (k === 'content-type') continue
if (k === 'set-cookie') {
_res.headers.append('set-cookie', v)
} else {
_res.headers.set(k, v)
}
}
}
this.#res = _res
this.finalized = true
}Output with the corrected fixBoth scenarios pass. Would appreciate a re-check on this — happy to provide any additional info. |
Fixes #4992
What this fixes
When middleware prepares cookies before
next()and the handler assigns a rawResponsetoc.res, the preparedSet-Cookieheaders can be lost ifc.reshas not been initialized yet.Cause
The
Context.ressetter only merged headers fromthis.#res. In this path, cookies added bysetCookie(c, ...)are still in#preparedHeaders, so assigning a rawResponseskips them.Fix
Set-Cookievalues when assigning a rawResponsebeforec.resis initialized.c.reshas already been read.The author should do the following, if applicable
bun run format:fix && bun run lint:fixto format the codeVerified locally with:
npx vitest --run src/context.test.ts -t "raw Response"npx vitest --run src/hono.test.ts -t "Overwrite the response from middleware after next"npx vitest --run src/hono.test.ts src/context.test.tsnpx eslint src/context.ts src/context.test.tsnpx tsc --noEmit --pretty falsenpx prettier --check src/context.ts src/context.test.tsgit diff --checkRemove-Item Env:NO_COLOR -ErrorAction SilentlyContinue; npx vitest --runFull Vitest result: 144 files passed, 4525 tests passed, 33 skipped.