[GH-ISSUE #8127] customSession plugin double-encodes Set-Cookie values during session refresh #11004

Closed
opened 2026-04-13 07:22:59 -05:00 by GiteaMirror · 2 comments
Owner

Originally created by @coldter on GitHub (Feb 24, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/8127

Note: This issue was generated with AI assistance (Claude Code).

Description

The customSession plugin's /get-session endpoint override double-encodes cookie values when forwarding Set-Cookie headers from the internal getSession() call. The browser receives a double-encoded cookie after a session refresh, causing "session not found" errors on subsequent requests.

Reproduction

  1. Configure betterAuth with a customSession plugin (e.g., to enrich sessions with permissions).
  2. Sign in via /api/auth/sign-in/email.
  3. Observe the correct Set-Cookie header after sign-in:
set-cookie: session_token_v1=BCwy6jS9Fm3FfpOoKlPlzrHxHvojZPtE.rNpUR%2B9TXuJJHbqi2lkVwTjVWi%2FTVU3I1nLlWr5t4MU%3D; Max-Age=604800; Path=/; HttpOnly; SameSite=Lax
  1. Call /api/auth/get-session (this triggers a session refresh).
  2. Observe the double-encoded Set-Cookie header returned by the /get-session override:
set-cookie: session_token_v1=BCwy6jS9Fm3FfpOoKlPlzrHxHvojZPtE.rNpUR%252B9TXuJJHbqi2lkVwTjVWi%252FTVU3I1nLlWr5t4MU%253D; Max-Age=3599; Path=/; HttpOnly; SameSite=Lax
  1. Subsequent requests fail with "session not found" because the browser sends the double-encoded cookie (e.g., %252B) which does not match the token stored in the database.

Note: %2B is the correct encoding of +; %252B is %2B encoded again (%%25).


Root cause

In src/plugins/custom-session/index.ts the /get-session endpoint override reads session.headers.getSetCookie() and re-registers each cookie via ctx.setCookie(...):

for (const cookieStr of session.headers.getSetCookie())
  parseSetCookieHeader(cookieStr).forEach((attrs, name) => {
    ctx.setCookie(name, attrs.value, {
      maxAge: attrs["max-age"],
      expires: attrs.expires,
      domain: attrs.domain,
      path: attrs.path,
      secure: attrs.secure,
      httpOnly: attrs.httponly,
      sameSite: attrs.samesite,
    });
  });

Why this double-encodes:

  1. The internal getSession() sets the cookie using setSignedCookie()signCookieValue()encodeURIComponent() — producing a correctly percent-encoded value (e.g., %2B for +).
  2. parseSetCookieHeader() extracts attrs.value directly from the raw Set-Cookie header string without decoding, so the value still contains %2B.
  3. ctx.setCookie() (via better-call's serializeCookie()) calls encodeURIComponent() again — encoding % to %25 and turning %2B into %252B.

Environment

  • better-auth: 1.4.19 (also reproduced in 1.5.0-beta.18)
  • better-call: 1.1.8
  • Framework: Hono (OpenAPIHono)
  • Runtime: Bun

Originally created by @coldter on GitHub (Feb 24, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/8127 > **Note:** This issue was generated with AI assistance (Claude Code). ## Description The `customSession` plugin's `/get-session` endpoint override double-encodes cookie values when forwarding `Set-Cookie` headers from the internal `getSession()` call. The browser receives a double-encoded cookie after a session refresh, causing **"session not found"** errors on subsequent requests. ## Reproduction 1. Configure `betterAuth` with a `customSession` plugin (e.g., to enrich sessions with permissions). 2. Sign in via `/api/auth/sign-in/email`. 3. Observe the correct `Set-Cookie` header after sign-in: ```http set-cookie: session_token_v1=BCwy6jS9Fm3FfpOoKlPlzrHxHvojZPtE.rNpUR%2B9TXuJJHbqi2lkVwTjVWi%2FTVU3I1nLlWr5t4MU%3D; Max-Age=604800; Path=/; HttpOnly; SameSite=Lax ``` 4. Call `/api/auth/get-session` (this triggers a session refresh). 5. Observe the **double-encoded** `Set-Cookie` header returned by the `/get-session` override: ```http set-cookie: session_token_v1=BCwy6jS9Fm3FfpOoKlPlzrHxHvojZPtE.rNpUR%252B9TXuJJHbqi2lkVwTjVWi%252FTVU3I1nLlWr5t4MU%253D; Max-Age=3599; Path=/; HttpOnly; SameSite=Lax ``` 6. Subsequent requests fail with **"session not found"** because the browser sends the double-encoded cookie (e.g., `%252B`) which does not match the token stored in the database. > Note: `%2B` is the correct encoding of `+`; `%252B` is `%2B` encoded again (`%` → `%25`). --- ## Root cause In `src/plugins/custom-session/index.ts` the `/get-session` endpoint override reads `session.headers.getSetCookie()` and re-registers each cookie via `ctx.setCookie(...)`: ```ts for (const cookieStr of session.headers.getSetCookie()) parseSetCookieHeader(cookieStr).forEach((attrs, name) => { ctx.setCookie(name, attrs.value, { maxAge: attrs["max-age"], expires: attrs.expires, domain: attrs.domain, path: attrs.path, secure: attrs.secure, httpOnly: attrs.httponly, sameSite: attrs.samesite, }); }); ``` Why this double-encodes: 1. The internal `getSession()` sets the cookie using `setSignedCookie()` → `signCookieValue()` → `encodeURIComponent()` — producing a correctly percent-encoded value (e.g., `%2B` for `+`). 2. `parseSetCookieHeader()` extracts `attrs.value` directly from the raw `Set-Cookie` header string **without decoding**, so the value still contains `%2B`. 3. `ctx.setCookie()` (via `better-call`'s `serializeCookie()`) calls `encodeURIComponent()` again — encoding `%` to `%25` and turning `%2B` into `%252B`. --- ## Environment * **better-auth:** `1.4.19` (also reproduced in `1.5.0-beta.18`) * **better-call:** `1.1.8` * **Framework:** Hono (OpenAPIHono) * **Runtime:** Bun ---
GiteaMirror added the lockedbug labels 2026-04-13 07:22:59 -05:00
Author
Owner

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

Hi @coldter! I'm Dosu and I’m helping the better-auth team.

This looks like a legitimate bug. Your analysis is spot on — the root cause is exactly as you described:

  1. parseSetCookieHeader() extracts cookie values without decoding them
  2. ctx.setCookie() applies encodeURIComponent() again (confirmed by a comment in the oauth-proxy plugin: "URI-decoded value because ctx.setCookie will URI-encode it again")

The fix should be to decode the value before passing it to ctx.setCookie(). Something like:

ctx.setCookie(name, decodeURIComponent(attrs.value), { ... });

Or alternatively, parseSetCookieHeader() could decode values before returning them.

Related context: A similar cookie handling bug was recently fixed in PR #7879 (the comma-joining issue). The fix used Headers.getSetCookie() instead of Headers.get(), but it didn't address the encoding issue you've identified.

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:3952054280 --> @dosubot[bot] commented on GitHub (Feb 24, 2026): <!-- Greeting --> Hi @coldter! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> This looks like a legitimate bug. Your analysis is spot on — the root cause is exactly as you described: 1. [`parseSetCookieHeader()`](https://github.com/better-auth/better-auth/blob/1892ca3b0081afeaa4e3fb5c5e389a821f7fd863/packages/better-auth/src/cookies/cookie-utils.ts#L73-L135) extracts cookie values **without decoding** them 2. `ctx.setCookie()` applies `encodeURIComponent()` again (confirmed by a comment in the [oauth-proxy plugin](https://github.com/better-auth/better-auth/blob/1892ca3b0081afeaa4e3fb5c5e389a821f7fd863/packages/better-auth/src/plugins/oauth-proxy/index.ts#L210-L216): *"URI-decoded value because `ctx.setCookie` will URI-encode it again"*) The fix should be to decode the value before passing it to `ctx.setCookie()`. Something like: ```ts ctx.setCookie(name, decodeURIComponent(attrs.value), { ... }); ``` Or alternatively, `parseSetCookieHeader()` could decode values before returning them. **Related context**: A [similar cookie handling bug](https://github.com/better-auth/better-auth/issues/7878) was recently fixed in [PR #7879](https://github.com/better-auth/better-auth/pull/7879) (the comma-joining issue). The fix used `Headers.getSetCookie()` instead of `Headers.get()`, but it didn't address the encoding issue you've identified. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/ceb31799-6de7-40d9-8ae2-46b8d219a6c0?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/ceb31799-6de7-40d9-8ae2-46b8d219a6c0?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/ceb31799-6de7-40d9-8ae2-46b8d219a6c0?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/ceb31799-6de7-40d9-8ae2-46b8d219a6c0?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/ceb31799-6de7-40d9-8ae2-46b8d219a6c0?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/ceb31799-6de7-40d9-8ae2-46b8d219a6c0?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/ceb31799-6de7-40d9-8ae2-46b8d219a6c0?feedback_type=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)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=This%20looks%20like%20a%20legitimate%20bug.%20Your%20analysis%20is%20spot%20on%20%E2%80%94%20the%20root%20cause%20is%20exactly%20as%20you%20described%3A%0A%0A1.%20%5B%60parseSetCookieHeader%28%29%60%5D%28https%3A//github.com/better-auth/better-auth/blob/1892ca3b0081afeaa4e3fb5c5e389a821f7fd863/packages/better-auth/src/cookies/cookie-utils.ts%23L73-L135%29%20extracts%20cookie%20values%20%2A%2Awithout%20decoding%2A%2A%20them%0A2.%20%60ctx.setCookie%28%29%60%20applies%20%60encodeURIComponent%28%29%60%20again%20%28confirmed%20by%20a%20comment%20in%20the%20%5Boauth-proxy%20plugin%5D%28https%3A//github.com/better-auth/better-auth/blob/1892ca3b0081afeaa4e3fb5c5e389a821f7fd863/packages/better-auth/src/plugins/oauth-proxy/index.ts%23L210-L216%29%3A%20%2A%22URI-decoded%20value%20because%20%60ctx.setCookie%60%20will%20URI-encode%20it%20again%22%2A%29%0A%0AThe%20fix%20should%20be%20to%20decode%20the%20value%20before%20passing%20it%20to%20%60ctx.setCookie%28%29%60.%20Something%20like%3A%0A%0A%60%60%60ts%0Actx.setCookie%28name%2C%20decodeURIComponent%28attrs.value%29%2C%20%7B%20...%20%7D%29%3B%0A%60%60%60%0A%0AOr%20alternatively%2C%20%60parseSetCookieHeader%28%29%60%20could%20decode%20values%20before%20returning%20them.%0A%0A%2A%2ARelated%20context%2A%2A%3A%20A%20%5Bsimilar%20cookie%20handling%20bug%5D%28https%3A//github.com/better-auth/better-auth/issues/7878%29%20was%20recently%20fixed%20in%20%5BPR%20%237879%5D%28https%3A//github.com/better-auth/better-auth/pull/7879%29%20%28the%20comma-joining%20issue%29.%20The%20fix%20used%20%60Headers.getSetCookie%28%29%60%20instead%20of%20%60Headers.get%28%29%60%2C%20but%20it%20didn%27t%20address%20the%20encoding%20issue%20you%27ve%20identified.)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&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/8127)
Author
Owner

@maestrodrew commented on GitHub (Mar 2, 2026):

I have experienced this issue as well. However, in my case, I am unable to reproduce the issue consistently. It seems to occur at random which leads me to believe there could be some sort of race condition/timing issue in play here. Whenever the issue occurs though, the session token cookie is always double encoded with an extra %25 at the end of the token

<!-- gh-comment-id:3981535120 --> @maestrodrew commented on GitHub (Mar 2, 2026): I have experienced this issue as well. However, in my case, I am unable to reproduce the issue consistently. It seems to occur at random which leads me to believe there could be some sort of race condition/timing issue in play here. Whenever the issue occurs though, the session token cookie is always double encoded with an extra `%25` at the end of the token
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#11004