Skip to content

fix(next-auth): detect Node responses by a callable headers.append - #13486

Open
endlessdev wants to merge 1 commit into
nextauthjs:mainfrom
endlessdev:fix/set-cookie-node-response-detection
Open

fix(next-auth): detect Node responses by a callable headers.append#13486
endlessdev wants to merge 1 commit into
nextauthjs:mainfrom
endlessdev:fix/set-cookie-node-response-detection

Conversation

@endlessdev

Copy link
Copy Markdown

☕️ Reasoning

initAuth decides how to attach set-cookie by duck-typing the response object:

if ("headers" in response) response.headers.append("set-cookie", cookie)
else response.appendHeader("set-cookie", cookie)

Node's http.ServerResponse has no headers property, so on Node this correctly
falls through to appendHeader. But the check is not portable: Bun's node:http
ServerResponse does expose headers, and it is a plain object rather than a
Headers instance.
The check takes the Web branch and throws:

TypeError: response.headers.append is not a function
  (In 'response.headers.append("set-cookie", cookie)')

Thirty-second reproduction, no next-auth involved:

// repro.js
const http = require("http")
const server = http.createServer((req, res) => {
  console.log({
    hasHeaders: "headers" in res,               // the current discriminator
    headersAppend: typeof res.headers?.append,  // what the Web branch calls
    appendHeader: typeof res.appendHeader,      // what the Node branch calls
  })
  res.end("ok")
  server.close()
})
server.listen(0, "127.0.0.1", () => fetch(`http://127.0.0.1:${server.address().port}/`))
$ node repro.js
{ hasHeaders: false, headersAppend: 'undefined', appendHeader: 'function' }

$ bun repro.js
{ hasHeaders: true,  headersAppend: 'undefined', appendHeader: 'function' }

So on Bun the discriminator says "Web Response", the Web branch is taken, and
headers.append is undefined.

Impact

Every Pages Router route that reads the session via auth(req, res) (also
getServerSideProps) returns 500 on Bun. In our app that was 19 routes,
including wallet, subscription, and certification endpoints.

What makes this nasty is that nothing fails loudly: the app boots, health
checks pass, and a rolling deploy completes green. We only found it by probing
the routes directly and comparing status codes against Node:

route Node Bun
/api/v1/wallet/card 401 500
/api/v1/subscription/list 401 500
/api/v1/short-term/list 200 500
POST /api/v1/slack 403 500

Fix

Require a callable append before taking the Web branch:

if ("headers" in response && typeof response.headers?.append === "function") {
  response.headers.append("set-cookie", cookie)
} else {
  response.appendHeader("set-cookie", cookie)
}
  • Real Response objects (App Router) still take the Headers path — unchanged.
  • Every ServerResponse falls through to appendHeader, which Node (≥18.3) and
    Bun both implement.

The two duplicated call sites are folded into one appendSetCookie helper so the
reasoning lives next to the check.

Verified against next-auth@5.0.0-beta.32 in production: with this change Bun
returns exactly the same status codes as Node on all of the routes above, and the
TypeError disappears from the logs.

🧢 Checklist

  • Documentation
  • Tests
  • Ready to be merged

I did not add a unit test: exercising this line requires getSession to return a
response that actually carries set-cookie, which needs more mocking than the
existing suite sets up. Happy to add one if you'd like it — just say which shape
you'd prefer (mocking getSession, or an e2e run under Bun).

🎫 Affected issues

I could not find an existing issue for this.

📌 Resources

`"headers" in response` is used to tell a Web `Response` from a Node
`ServerResponse` before appending `set-cookie`. Node's `ServerResponse` has no
`headers` property, so the check works there — but it is not portable.

Bun's `node:http` `ServerResponse` *does* expose `headers`, and it is a plain
object rather than a `Headers` instance. The check therefore takes the Web
branch and throws:

    TypeError: response.headers.append is not a function

Every session-reading Pages Router API route (`auth(req, res)`,
`getServerSideProps`) returns a 500 as a result. The process still boots and
health checks still pass, so this fails silently in production.

Require a callable `append` instead. Real `Response` objects keep the `Headers`
path; every `ServerResponse` falls through to `appendHeader`, which Node and Bun
both implement.
@vercel

vercel Bot commented Aug 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
auth-docs Ready Ready Preview Aug 25, 2026 1:56pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
next-auth-docs Ignored Ignored Preview Aug 25, 2026 1:56pm

Request Review

@vercel

vercel Bot commented Aug 25, 2026

Copy link
Copy Markdown

@endlessdev is attempting to deploy a commit to the authjs Team on Vercel.

A member of the Team first needs to authorize it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant