Problem
In v3, session cookie persistence moved from an on-headers hook (which fires at res.writeHead() time) to a res.end() wrapper. This means any response that flushes headers before res.end() — via res.flushHeaders(), res.write(), res.writeHead(), res.sendFile(), or res.download() — will have res.headersSent === true by the time the cookie write runs, and the session cookie is silently dropped.
This affects any application using streaming/chunked responses, which is a common pattern for server-side rendered applications that flush the <head> early to allow the browser to start loading assets while the body renders.
There is no error, no warning, and no workaround. The session just stops being persisted.
Expected behavior
The session cookie should be written regardless of whether the response is streamed or buffered. The v2 behavior (using on-headers) handled this correctly.
Reproduction
app.use(auth(config));
app.get('/', (req, res) => {
res.flushHeaders(); // headers sent
res.write('<html>...'); // streaming body
// ... later ...
res.end('</html>'); // session cookie write runs here, but headersSent is true — cookie lost
});
Suggested fix
Expose a mechanism to write the session cookie before the response starts streaming. Either:
-
Restore on-headers as the default — this was the v2 behavior, it fires at writeHead() time (the last moment headers are mutable), and it works with both buffered and streamed responses.
-
Expose a public API like req.oidc.persistSession(res) that consumers can call explicitly when they know headers are about to flush. This would allow streaming applications to call it at the right moment without relying on internal timing.
Option 1 is backward-compatible and covers all cases transparently. Option 2 gives consumers control but requires awareness of the issue.
Environment
express-openid-connect: v3 (breaking change from v2)
- Node.js: >=22
- Express: 4.x
- Use case: streaming SSR with
res.flushHeaders() before res.end()
Problem
In v3, session cookie persistence moved from an
on-headershook (which fires atres.writeHead()time) to ares.end()wrapper. This means any response that flushes headers beforeres.end()— viares.flushHeaders(),res.write(),res.writeHead(),res.sendFile(), orres.download()— will haveres.headersSent === trueby the time the cookie write runs, and the session cookie is silently dropped.This affects any application using streaming/chunked responses, which is a common pattern for server-side rendered applications that flush the
<head>early to allow the browser to start loading assets while the body renders.There is no error, no warning, and no workaround. The session just stops being persisted.
Expected behavior
The session cookie should be written regardless of whether the response is streamed or buffered. The v2 behavior (using
on-headers) handled this correctly.Reproduction
Suggested fix
Expose a mechanism to write the session cookie before the response starts streaming. Either:
Restore
on-headersas the default — this was the v2 behavior, it fires atwriteHead()time (the last moment headers are mutable), and it works with both buffered and streamed responses.Expose a public API like
req.oidc.persistSession(res)that consumers can call explicitly when they know headers are about to flush. This would allow streaming applications to call it at the right moment without relying on internal timing.Option 1 is backward-compatible and covers all cases transparently. Option 2 gives consumers control but requires awareness of the issue.
Environment
express-openid-connect: v3 (breaking change from v2)res.flushHeaders()beforeres.end()