[GH-ISSUE #8763] getSessionFromCtx discards session_data cookie refresh — JWE session expires after maxAge with no renewal #11184

Open
opened 2026-04-13 07:32:32 -05:00 by GiteaMirror · 1 comment
Owner

Originally created by @Lookwe69 on GitHub (Mar 24, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/8763

Summary

getSessionFromCtx in api/routes/session.ts calls getSession() with returnHeaders: false.
This creates an isolated inner better-call context with its own Headers instance, so any
set-cookie headers written during that call (including the session_data cookie refresh
triggered by setCookieCache) are silently discarded when the call returns.

Affected configuration

  • session.cookieCache.enabled: true
  • session.cookieCache.strategy: "jwe" (or any strategy)
  • Any endpoint that calls getSessionFromCtx internally (e.g. getAccessToken)

Reproduction

  1. Configure cookie cache with a short maxAge (e.g. 300s, the default).
  2. Sign in — session_data cookie is set with 300s maxAge.
  3. Call an endpoint that goes through getSessionFromCtx (e.g. GET /api/auth/get-access-token) when timeUntilExpiry < updateAge (i.e. within the last 60s of the 300s window).
  4. The inner getSession() detects expiry and calls setCookieCache, but the refreshed cookie is written to the inner context's headers and never forwarded to the response.
  5. After 300 seconds, the browser no longer sends session_data. All subsequent requests see no cached session and return 401.

Root cause

// src/api/routes/session.ts
const getSessionFromCtx = async (ctx, config) => {
  if (ctx.context.session) return ctx.context.session;
  const session = await getSession()({
    ...ctx,
    returnHeaders: false,  // ← inner set-cookie headers are LOST
    ...
  }).catch(() => null);
  ctx.context.session = session;
  return session;
};

better-call's createInternalContext always creates const headers = new Headers() and
overrides setCookie with a closure over that local instance. With returnHeaders: false the
caller only gets the response body — the accumulated set-cookie headers are thrown away.

Proposed fix

Switch to returnHeaders: true and copy the inner set-cookie headers back onto the outer
context's responseHeaders:

const getSessionFromCtx = async (ctx, config) => {
  if (ctx.context.session) return ctx.context.session;
  const result = await getSession()({
    ...ctx,
    returnHeaders: true,   // expose inner headers
    ...
  }).catch(() => null);

  // Propagate cookie refreshes to the outer response
  if (result?.headers instanceof Headers && ctx.responseHeaders instanceof Headers) {
    const setCookies = typeof result.headers.getSetCookie === "function"
      ? result.headers.getSetCookie()
      : [];
    for (const cookie of setCookies) {
      ctx.responseHeaders.append("set-cookie", cookie);
    }
  }

  const session = result && "response" in result ? result.response : result;
  ctx.context.session = session;
  return session;
};

Impact

Without this fix, users are silently disconnected every cookieCache.maxAge seconds (default
300s / 5 minutes) in any stateless cookie-cache setup, because the refresh never reaches the
browser.

Disclaimer : This issue was written with the help of Claude Code.

Thanks a lot for your (amazing) work

Originally created by @Lookwe69 on GitHub (Mar 24, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/8763 ## Summary `getSessionFromCtx` in `api/routes/session.ts` calls `getSession()` with `returnHeaders: false`. This creates an isolated inner better-call context with its own `Headers` instance, so any `set-cookie` headers written during that call (including the `session_data` cookie refresh triggered by `setCookieCache`) are silently discarded when the call returns. ## Affected configuration - `session.cookieCache.enabled: true` - `session.cookieCache.strategy: "jwe"` (or any strategy) - Any endpoint that calls `getSessionFromCtx` internally (e.g. `getAccessToken`) ## Reproduction 1. Configure cookie cache with a short `maxAge` (e.g. 300s, the default). 2. Sign in — `session_data` cookie is set with 300s maxAge. 3. Call an endpoint that goes through `getSessionFromCtx` (e.g. `GET /api/auth/get-access-token`) when `timeUntilExpiry < updateAge` (i.e. within the last 60s of the 300s window). 4. The inner `getSession()` detects expiry and calls `setCookieCache`, but the refreshed cookie is written to the inner context's headers and never forwarded to the response. 5. After 300 seconds, the browser no longer sends `session_data`. All subsequent requests see no cached session and return 401. ## Root cause ```ts // src/api/routes/session.ts const getSessionFromCtx = async (ctx, config) => { if (ctx.context.session) return ctx.context.session; const session = await getSession()({ ...ctx, returnHeaders: false, // ← inner set-cookie headers are LOST ... }).catch(() => null); ctx.context.session = session; return session; }; ``` `better-call`'s `createInternalContext` always creates `const headers = new Headers()` and overrides `setCookie` with a closure over that local instance. With `returnHeaders: false` the caller only gets the response body — the accumulated `set-cookie` headers are thrown away. ## Proposed fix Switch to `returnHeaders: true` and copy the inner `set-cookie` headers back onto the outer context's `responseHeaders`: ```ts const getSessionFromCtx = async (ctx, config) => { if (ctx.context.session) return ctx.context.session; const result = await getSession()({ ...ctx, returnHeaders: true, // expose inner headers ... }).catch(() => null); // Propagate cookie refreshes to the outer response if (result?.headers instanceof Headers && ctx.responseHeaders instanceof Headers) { const setCookies = typeof result.headers.getSetCookie === "function" ? result.headers.getSetCookie() : []; for (const cookie of setCookies) { ctx.responseHeaders.append("set-cookie", cookie); } } const session = result && "response" in result ? result.response : result; ctx.context.session = session; return session; }; ``` ## Impact Without this fix, users are silently disconnected every `cookieCache.maxAge` seconds (default 300s / 5 minutes) in any stateless cookie-cache setup, because the refresh never reaches the browser. Disclaimer : This issue was written with the help of Claude Code. Thanks a lot for your (amazing) work
GiteaMirror added the corebug labels 2026-04-13 07:32:32 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Mar 24, 2026):

Your analysis is spot-on. I can confirm that getSessionFromCtx in session.ts explicitly passes returnHeaders: false, which causes the set-cookie headers from setCookieCache to be discarded.

This is actually a known issue with several related reports:

Your proposed fix using returnHeaders: true and forwarding cookies via getSetCookie() aligns with PR #7879 which fixed a critical bug where headers.get("set-cookie") was comma-joining multiple cookies and corrupting attributes.

The maintainer has mentioned working on a pulse mechanism to periodically hit /get-session from the client to refresh cookies automatically, but that's not yet available.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other

Chat with Dosu Open in Cursor Join Discord Share on X

<!-- gh-comment-id:4120907901 --> @dosubot[bot] commented on GitHub (Mar 24, 2026): <!-- Answer --> Your analysis is spot-on. I can confirm that [`getSessionFromCtx` in `session.ts`](https://github.com/better-auth/better-auth/blob/03278b5ad62c4cea61a8373bb7fe522519b31a42/packages/better-auth/src/api/routes/session.ts#L525-L563) explicitly passes `returnHeaders: false`, which causes the `set-cookie` headers from `setCookieCache` to be discarded. This is actually a known issue with several related reports: - [#2115 - Session cookie maxAge is not refreshed](https://github.com/better-auth/better-auth/issues/2115#issuecomment-3487928118) - maintainer @Bekacru confirmed the root cause - [#4389 - Cookie cache not updated from auth.api.getSession](https://github.com/better-auth/better-auth/issues/4389) - same scenario with TanStack Start - [#7607](https://github.com/better-auth/better-auth/issues/7607), [#3713](https://github.com/better-auth/better-auth/issues/3713), [#7878](https://github.com/better-auth/better-auth/issues/7878) - related issues with different plugin combinations Your proposed fix using `returnHeaders: true` and forwarding cookies via `getSetCookie()` aligns with [PR #7879](https://github.com/better-auth/better-auth/pull/7879) which fixed a critical bug where `headers.get("set-cookie")` was comma-joining multiple cookies and corrupting attributes. The maintainer has mentioned working on a **pulse mechanism** to periodically hit `/get-session` from the client to refresh cookies automatically, but that's not yet available. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=brand-link).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/76221a0d-f717-4420-a919-fe6c6867b86c?feedback_type=great_response&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/76221a0d-f717-4420-a919-fe6c6867b86c?feedback_type=irrelevant_answer&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/76221a0d-f717-4420-a919-fe6c6867b86c?feedback_type=incorrect_sources&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/76221a0d-f717-4420-a919-fe6c6867b86c?feedback_type=too_verbose&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/76221a0d-f717-4420-a919-fe6c6867b86c?feedback_type=hallucination&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/76221a0d-f717-4420-a919-fe6c6867b86c?feedback_type=bug_report&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-bug_report) | [Other](https://app.dosu.dev/response-feedback/76221a0d-f717-4420-a919-fe6c6867b86c?feedback_type=other&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-other)</sup> [![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=chat-badge)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=Your%20analysis%20is%20spot-on.%20I%20can%20confirm%20that%20%5B%60getSessionFromCtx%60%20in%20%60session.ts%60%5D%28https%3A//github.com/better-auth/better-auth/blob/03278b5ad62c4cea61a8373bb7fe522519b31a42/packages/better-auth/src/api/routes/session.ts%23L525-L563%29%20explicitly%20passes%20%60returnHeaders%3A%20false%60%2C%20which%20causes%20the%20%60set-cookie%60%20headers%20from%20%60setCookieCache%60%20to%20be%20discarded.%0A%0AThis%20is%20actually%20a%20known%20issue%20with%20several%20related%20reports%3A%0A-%20%5B%232115%20-%20Session%20cookie%20maxAge%20is%20not%20refreshed%5D%28https%3A//github.com/better-auth/better-auth/issues/2115%23issuecomment-3487928118%29%20-%20maintainer%20%40Bekacru%20confirmed%20the%20root%20cause%0A-%20%5B%234389%20-%20Cookie%20cache%20not%20updated%20from%20auth.api.getSession%5D%28https%3A//github.com/better-auth/better-auth/issues/4389%29%20-%20same%20scenario%20with%20TanStack%20Start%0A-%20%5B%237607%5D%28https%3A//github.com/better-auth/better-auth/issues/7607%29%2C%20%5B%233713%5D%28https%3A//github.com/better-auth/better-auth/issues/3713%29%2C%20%5B%237878%5D%28https%3A//github.com/better-auth/better-auth/issues/7878%29%20-%20related%20issues%20with%20different%20plugin%20combinations%0A%0AYour%20proposed%20fix%20using%20%60returnHeaders%3A%20true%60%20and%20forwarding%20cookies%20via%20%60getSetCookie%28%29%60%20aligns%20with%20%5BPR%20%237879%5D%28https%3A//github.com/better-auth/better-auth/pull/7879%29%20which%20fixed%20a%20critical%20bug%20where%20%60headers.get%28%22set-cookie%22%29%60%20was%20comma-joining%20multiple%20cookies%20and%20corrupting%20attributes.%0A%0AThe%20maintainer%20has%20mentioned%20working%20on%20a%20%2A%2Apulse%20mechanism%2A%2A%20to%20periodically%20hit%20%60/get-session%60%20from%20the%20client%20to%20refresh%20cookies%20automatically%2C%20but%20that%27s%20not%20yet%20available.)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=join-discord)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/8763)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#11184