Skip to content

fix: txn cookie accumulation - #2748

Open
Piyush-85 wants to merge 15 commits into
mainfrom
fix/txn-accumulation
Open

fix: txn cookie accumulation#2748
Piyush-85 wants to merge 15 commits into
mainfrom
fix/txn-accumulation

Conversation

@Piyush-85

@Piyush-85 Piyush-85 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor
  • All new/changed/fixed functionality is covered by tests (or N/A)
  • I have added documentation for all new/changed functionality (or N/A)

📋 Changes

Fixes unbounded _txn* transaction-cookie accumulation that produces 431 Request Header Fields Too Large, and adds a warning for the other main cause of oversized request headers - a large session cookie.

  1. Prefetch guard - stop creating transaction cookies for flows that never complete
  • handler() now returns 204 (empty body) on GET /auth/login when the request is a known Next.js prefetch, instead of running handleLogin and writing a txn cookie that will never be consumed.
  • Detection is via a new exported helper isNonNavigationalRequest(req) (src/utils/request.ts), which matches explicit prefetch headers only: next-router-prefetch, purpose: prefetch, sec-purpose (substring match, to also catch Chromium's prefetch;prerender), and x-middleware-prefetch. It intentionally does not key off sec-fetch-mode or accept: text/x-component, to avoid blocking legitimate fetch()/XHR calls and real RSC navigations to /auth/login.
  1. Bounded, FIFO eviction of transaction cookies
  • Transaction cookie values are now encoded as {ts}:{jwe} (unix-seconds creation timestamp prefix). This enables O(1) oldest-first ordering during eviction with no decryption and no cookie-name change.
  • On save(), when the combined size of all _txn* cookies exceeds 3500 bytes, the oldest are evicted (FIFO) until back under the limit, before the new cookie is written. The cookie being (re)written for the current state is never evicted.
  • Backward compatible reads: get() strips the {ts}: prefix before decrypting; legacy bare {jwe} values (no prefix) are still decrypted, and are treated as timestamp 0 (evicted first) during cleanup.
  • Callback cleanup delete removes only the completing flow's txn{state}, preserving in-flight logins in other tabs (multi-tab safe).
  1. Session-cookie size warning (new)
  • StatelessSessionStore.set() now console.warns when the total encoded size of the _session chunks reaches SESSION_COOKIE_SIZE_WARN_BYTES (4096 bytes), advising the developer to remove unnecessary custom claims or switch to a stateful session.
  • Why: unlike transaction cookies, the session is never evicted, so a large session is the primary remaining cause of 431. Previously the session cookie had no size check at all, the existing 4096-byte check in storeInCookie only ever ran for connection-token (_FC*) cookies, and its sessionCookieName branch was unreachable dead code (now removed).
  • This is diagnostic only (a warning); it changes no cookie-writing behavior and makes no assumption about the platform header limit.

Docs (EXAMPLES.md + README.md)

  • Consolidated all 431 guidance under one section, renamed to "Preventing '431 Request Header Fields Too Large' Errors", and added it to the table of contents. It documents the prefetch 204 guard, the fixed-limit FIFO eviction, the two recommended practices
    - use <a>/<Link prefetch={false}> — not <Link href="/auth/login">
    - prefer withPageAuthRequired over middleware redirects
    - recommends lowering transactionCookie.maxAge if in-flight logins are being evicted too aggressively.

📎 References

🎯 Testing

  • src/server/txn-cookie-accumulation.test.ts — prefetch-header detection (positive/negative), 204 + no cookie written on prefetch, real navigation allowed through, FIFO eviction at the fixed 3500-byte limit, oldest-first ordering incl. legacy timestamp=0 values, prefix-scoped eviction, {ts}:{jwe} encoding/decoding round-trip, callback deletes only the completing cookie (multi-tab preserved), and a @ts-expect-error guard proving maxSizeBytes is nolonger an accepted option.
  • src/server/session/stateless-session-store.test.ts — warns on an oversized (multi-chunk) session, does not warn on a normal session.
  • src/server/transaction-store.test.ts, redundant-txn-cookie-deletion.test.ts, auth-client.test.ts, mfa-popup.test.ts updated for the value-format and eviction changes.

Summary by CodeRabbit

  • Bug Fixes
    • Blocked Next.js prefetch/non-navigational login requests early to reduce “431 Request Header Fields Too Large” errors.
    • Added transaction-cookie size protection with FIFO eviction (including legacy handling), improved single-transaction retries, and ensured callback cleanup only removes the completing flow’s cookie.
    • Added warnings for oversized session cookies.
  • New Features
    • Added a utility to detect prefetch-style requests.
  • Documentation
    • Expanded Next.js 16 login-link guidance and added a new “Preventing ‘431…’ Errors” section.
  • Tests
    • Updated transaction decoding for stored value prefixes and added coverage for transaction-cookie accumulation and session-cookie warning behavior.

@Piyush-85
Piyush-85 requested a review from a team as a code owner July 13, 2026 15:04
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR blocks transaction creation from Next.js prefetches, evicts accumulated transaction cookies, adds cookie-size warnings, updates transaction-cookie encoding and tests, forwards client options, and documents practices for avoiding 431 errors.

Changes

Transaction Cookie Controls

Layer / File(s) Summary
Prefetch login flow
src/utils/request.ts, src/server/auth-client.ts, src/server/txn-cookie-accumulation.test.ts
Non-navigational login requests return 401 without writing transaction cookies; navigational flows pass request cookies to transaction persistence and verify callback cleanup.
Transaction cookie storage
src/server/transaction-store.ts, src/server/*test.ts, src/test/utils.ts
Transaction cookies use timestamp-prefixed values, FIFO eviction under a 3500-byte limit, legacy-value handling, single-transaction overwrites, and updated decryption assertions.
Cookie size warnings
src/server/session/stateless-session-store.ts, src/server/session/stateless-session-store.test.ts
Session-cookie chunks are measured collectively and warnings are emitted at the 4096-byte threshold.
Client option wiring
src/server/client.ts
The AuthClientProvider forwards custom fetch, MFA token TTL, and CSP nonce options.
Documentation guidance
README.md, EXAMPLES.md
Login prefetch restrictions, page-protection recommendations, transaction-cookie options, and 431-error prevention behavior are documented.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

  • auth0/nextjs-auth0#1917 — Directly tracks transaction-cookie accumulation that eventually causes oversized request headers.
  • auth0/nextjs-auth0#2450 — Addresses 431 failures from prefetch-triggered transaction-cookie accumulation.

Possibly related PRs

  • auth0/nextjs-auth0#2670 — Both changes update passwordless transaction-cookie persistence through TransactionStore.

Suggested reviewers: tusharpandey13

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements the reported cookie-stacking fix by blocking prefetch login flows, evicting old transaction cookies, and cleaning up only the completed flow.
Out of Scope Changes check ✅ Passed The added docs, helpers, and tests all support the transaction-cookie accumulation fix and related 431 guidance.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: fixing transaction cookie accumulation.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/txn-accumulation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov-commenter

codecov-commenter commented Jul 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.74436% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.26%. Comparing base (6eb5e2d) to head (30bf3ed).

Files with missing lines Patch % Lines
src/server/session/stateless-session-store.ts 80.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2748      +/-   ##
==========================================
+ Coverage   87.98%   88.26%   +0.27%     
==========================================
  Files          80       80              
  Lines       11514    11611      +97     
  Branches     2386     2413      +27     
==========================================
+ Hits        10131    10248     +117     
+ Misses       1338     1318      -20     
  Partials       45       45              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@EXAMPLES.md`:
- Around line 4048-4051: The transaction-cookie eviction check must include the
incoming cookie size: update the logic calculating txnBytes or the cap condition
to evaluate existing transaction-cookie bytes plus newCookieValue’s serialized
size against the 3500-byte limit. Preserve FIFO eviction and leave
non-transaction cookies untouched, and add a boundary test covering a new cookie
that causes the combined total to reach or exceed the limit.

In `@README.md`:
- Around line 193-194: Align the login-link guidance in README.md with the
corresponding recommendation in EXAMPLES.md. Choose one tested approach—prefer
plain <a> links until the RSC-navigation detector is fixed, or consistently
document Link with prefetch disabled—and update the conflicting documentation so
both files prescribe the same behavior.

In `@src/utils/request.ts`:
- Around line 27-35: The isNonNavigationalRequest function incorrectly treats
the generic Accept header as a prefetch signal. Remove that check or require an
explicit prefetch indicator, then update the login snippets in EXAMPLES.md at
lines 237-238 and 4057-4068 to reflect the corrected navigation behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7a92bef5-5000-46b6-8039-d5704cdc15da

📥 Commits

Reviewing files that changed from the base of the PR and between 17bdeda and 1df8c4f.

📒 Files selected for processing (13)
  • EXAMPLES.md
  • README.md
  • src/server/auth-client.test.ts
  • src/server/auth-client.ts
  • src/server/client.ts
  • src/server/mfa-popup.test.ts
  • src/server/session/stateless-session-store.test.ts
  • src/server/session/stateless-session-store.ts
  • src/server/transaction-store.test.ts
  • src/server/transaction-store.ts
  • src/server/txn-cookie-accumulation.test.ts
  • src/test/utils.ts
  • src/utils/request.ts
💤 Files with no reviewable changes (1)
  • src/server/client.ts

Comment thread EXAMPLES.md
Comment thread README.md Outdated
Comment thread src/utils/request.ts
@Piyush-85 Piyush-85 changed the title fix txn cookie accumulation fix: txn cookie accumulation Jul 15, 2026
@Piyush-85
Piyush-85 force-pushed the fix/txn-accumulation branch from 73c819d to 27119c4 Compare July 27, 2026 15:00

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/server/txn-cookie-accumulation.test.ts (2)

230-234: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Negative "not deleted" assertions are vacuous. Both sites apply ?.maxAge to a cookie that is absent from the ResponseCookies jar when the SDK behaves correctly, so undefined !== 0 passes even if the surrounding behavior regresses.

  • src/server/txn-cookie-accumulation.test.ts#L230-L234: replace expect(resCookies.get(\_txn${newerState}`)?.maxAge).not.toBe(0)withexpect(resCookies.get(`_txn${newerState}`)).toBeUndefined()`.
  • src/server/txn-cookie-accumulation.test.ts#L644-L646: replace expect(callbackRes.cookies.get("__txn_tabB")?.maxAge).not.toBe(0) with expect(callbackRes.cookies.get("__txn_tabB")).toBeUndefined().
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/txn-cookie-accumulation.test.ts` around lines 230 - 234, Replace
the vacuous negative cookie assertions with direct absence checks: in
src/server/txn-cookie-accumulation.test.ts lines 230-234, assert resCookies.get
for newerState is undefined; in lines 644-646, assert
callbackRes.cookies.get("__txn_tabB") is undefined. Keep the surrounding
eviction and new-cookie assertions unchanged.

237-256: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate of the preceding eviction test.

This test uses identical fixtures and assertions to "evicts oldest cookie first when the 3500 byte limit is exceeded" (Lines 213-235), minus the newer-cookie check. Either drop it or make it genuinely distinct (e.g., three cookies where two must be evicted, asserting eviction order).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/txn-cookie-accumulation.test.ts` around lines 237 - 256, The test
“evicts oldest login cookies first (FIFO by timestamp)” duplicates the preceding
eviction coverage. Remove it, or make it distinct by exercising three cookies
that exceed the limit and asserting that the two oldest cookies are evicted in
timestamp order while the newest remains.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/server/txn-cookie-accumulation.test.ts`:
- Around line 230-234: Replace the vacuous negative cookie assertions with
direct absence checks: in src/server/txn-cookie-accumulation.test.ts lines
230-234, assert resCookies.get for newerState is undefined; in lines 644-646,
assert callbackRes.cookies.get("__txn_tabB") is undefined. Keep the surrounding
eviction and new-cookie assertions unchanged.
- Around line 237-256: The test “evicts oldest login cookies first (FIFO by
timestamp)” duplicates the preceding eviction coverage. Remove it, or make it
distinct by exercising three cookies that exceed the limit and asserting that
the two oldest cookies are evicted in timestamp order while the newest remains.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b254c038-2492-4f33-97f1-5e7590de6e67

📥 Commits

Reviewing files that changed from the base of the PR and between 73c819d and 27119c4.

📒 Files selected for processing (13)
  • EXAMPLES.md
  • README.md
  • src/server/auth-client.test.ts
  • src/server/auth-client.ts
  • src/server/client.ts
  • src/server/mfa-popup.test.ts
  • src/server/session/stateless-session-store.test.ts
  • src/server/session/stateless-session-store.ts
  • src/server/transaction-store.test.ts
  • src/server/transaction-store.ts
  • src/server/txn-cookie-accumulation.test.ts
  • src/test/utils.ts
  • src/utils/request.ts
🚧 Files skipped from review as they are similar to previous changes (12)
  • src/server/session/stateless-session-store.test.ts
  • src/server/transaction-store.test.ts
  • src/server/client.ts
  • src/test/utils.ts
  • src/utils/request.ts
  • README.md
  • src/server/session/stateless-session-store.ts
  • src/server/mfa-popup.test.ts
  • src/server/transaction-store.ts
  • src/server/auth-client.ts
  • EXAMPLES.md
  • src/server/auth-client.test.ts

Comment thread src/server/auth-client.ts
const method = req.method;

if (method === "GET" && sanitizedPathname === this.routes.login) {
if (isNonNavigationalRequest(req)) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Primary fix for the 431: Next.js prefetches to /auth/login were silently minting a __txn_* cookie each time. We now short-circuit prefetch requests before handleLogin runs, so no cookie is written. Real navigations are unaffected — see isNonNavigationalRequest for why only prefetch-exclusive headers are matched.

* @param newCookieValue - Value of that cookie; its size is included in the cap
* so a large new cookie can still trigger eviction.
*/
private evictOldestTransactionCookies(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the safety net for prefetches the header guard can't catch (CDN-stripped, router.prefetch()). Before writing a new txn cookie, we cap the total __txn_* size at 3500 bytes and evict the oldest first (FIFO), so accumulated cookies can never grow the header to 431. Only __txn_* cookies are touched; the one being written is never evicted.

// "{ts}:{jwe}" — no cookie name change, backward compatible with legacy bare "{jwe}".
const ts = Math.floor(Date.now() / 1000);
const newCookieName = this.getTransactionCookieName(transactionState.state);
const newCookieValue = `${ts}:${jwe}`;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cookie value is now {ts}:{jwe} (name unchanged). The timestamp gives eviction its FIFO order; get() strips the prefix before decrypting and legacy bare {jwe} values still read fine, so this is backward compatible.

resCookies: cookies.ResponseCookies,
transactionState: TransactionState,
reqCookies?: cookies.RequestCookies
reqCookies?: cookies.RequestCookies | cookies.ReadonlyRequestCookies

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional removal here: save() used to have a guard that rejected a second login in single-transaction mode. It was dead code on main (nothing ever passed reqCookies, so it never ran). Now that reqCookies is passed for eviction, keeping that guard would have activated it for the first time and broken login retries. Removing it keeps behavior identical to main (single-txn logins overwrite the fixed __txn_ cookie).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

v4: Infinitely stacking cookies

2 participants