Skip to content

Persist browser sessions without delaying hydration - #7363

Open
masenf wants to merge 5 commits into
codex/session-transportfrom
codex/session-frontend
Open

masenf wants to merge 5 commits into
codex/session-transportfrom
codex/session-frontend

Conversation

@masenf

@masenf masenf commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

Let hydration and ordinary events run while the socket-delivered session credential is exchanged for an HttpOnly cookie. Credentials are never written to browser storage; routing-token and freshness keys are scoped to the backend URL. Web Locks coordinate simultaneous cold tabs, and duplicate tabs adopt server-issued replacement tokens.

Idle timers and visibility restoration request fresh credentials over the authenticated socket. Failed HTTP exchanges retry with bounded backoff while readiness stays pending. Success releases waiting uploads and Enterprise HTTP helpers; teardown aborts the exchange and rejects readiness. When an existing cookie replaces the provisional token, readiness waits until the replacement socket connects.

Uploads reserve their cancellation controller before waiting, so cancellation works immediately. Waiting uploads do not block ordinary WebSocket events. Transport warmup for established sessions and CONNECT-packet hydration remain intact.

Depends on #7362; real browser and deployment coverage is in #7364. Enterprise helper compatibility is in reflex-dev/reflex-enterprise#240. Rebased on main d02ba5409.

Validation: all 13 frontend runtime/lifecycle wrapper tests pass, including the actual upload helper, idle renewal, retry recovery, cancellation, late responses after teardown, and cookie-replacement reconnects. The new failure/reconnect regressions fail before their fixes. Full assembled stack: 12,147 unit tests passed with 80.15% coverage.

Related to ENG-12919, ENG-9088.

@masenf
masenf added this pull request to stack #7365 September 30, 2026 00:41
@masenf masenf changed the title Persist in-band browser sessions without delaying hydration Persist browser sessions without delaying hydration Sep 30, 2026
@greptile-apps

greptile-apps Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 4/5

[High impact] Refactors session token storage and upload flow.

Fix the freshness-hint error path before merging so full browser storage cannot block uploads.

Findings

  1. P1 Full storage blocks uploads ▶

Summary

The PR persists socket-issued sessions in HttpOnly cookies without delaying hydration.

  • The browser hydrates while it saves a session cookie.
  • Uploads wait for the cookie but remain cancellable.

Reviews (8) · Last reviewed commit: "Keep HTTP readiness pending through cook..." · Reviewed by Greptile

Comment thread packages/reflex-base/src/reflex_base/.templates/web/utils/state.js Outdated
Comment thread packages/reflex-base/src/reflex_base/.templates/web/utils/state.js Outdated
Comment thread packages/reflex-base/src/reflex_base/.templates/web/utils/state.js Outdated
Comment thread packages/reflex-base/src/reflex_base/.templates/web/utils/state.js Outdated
Comment thread packages/reflex-base/src/reflex_base/.templates/web/utils/state.js Outdated
@masenf
masenf force-pushed the codex/session-frontend branch from 12859c9 to d42c9b7 Compare October 5, 2026 21:24
@linear-code

linear-code Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

ENG-9088

ENG-12919

@masenf
masenf force-pushed the codex/session-frontend branch from d42c9b7 to da3a484 Compare October 8, 2026 17:55
@masenf
masenf force-pushed the codex/session-frontend branch from da3a484 to a8786e0 Compare October 8, 2026 18:24
@masenf
masenf force-pushed the codex/session-frontend branch 3 times, most recently from f72f7b7 to c8b6820 Compare October 14, 2026 09:35
@masenf
masenf force-pushed the codex/session-frontend branch from c8b6820 to 579c45e Compare October 14, 2026 09:46
@masenf
masenf marked this pull request as ready for review October 14, 2026 09:54
@masenf
masenf requested a review from a team as a code owner October 14, 2026 09:54
Comment on lines +1296 to +1299
window.localStorage.setItem(
sessionRefreshStorageKey(),
Date.now() + refreshDelay,
);

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.

P1 Full storage blocks uploads

If localStorage is full, setItem throws after the cookie exchange succeeds. The catch block retries the HTTP request, but each successful retry hits the same storage error. waitForSession() never resolves, so queued uploads never start even though the browser has a valid cookie.

Treat the freshness hint as optional. A failed storage write should not prevent readiness from finishing or renewal from continuing.

@cubic-dev-ai cubic-dev-ai 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.

3 issues found across 9 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="tests/units/compiler/state_js.test.mjs">

<violation number="1" location="tests/units/compiler/state_js.test.mjs:37">
P3: This mock returns a fresh expiry for every localStorage key, so the warm-transport test still passes if `state.js` reads a misspelled or unscoped refresh key. Return the expiry only for `reflex:ws://localhost:8000/_event:expires_at`.</violation>
</file>

<file name="packages/reflex-base/news/+session-cookie-bootstrap.feature.md">

<violation number="1" location="packages/reflex-base/news/+session-cookie-bootstrap.feature.md:1">
P3: This overstates isolation: the bootstrap still reads the shared legacy `sessionStorage['token']` key, so an existing token can be reused across backend URLs. Limit this claim to the newly scoped keys.</violation>
</file>

<file name="packages/reflex-base/src/reflex_base/.templates/web/utils/state.js">

<violation number="1" location="packages/reflex-base/src/reflex_base/.templates/web/utils/state.js:1296">
P2: A full localStorage quota makes this write throw after the server has installed the cookie. The catch retries without resolving `initialSessionExchange`, leaving waiting uploads blocked despite a valid cookie. Treat the freshness hint as best-effort.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment on lines +1296 to +1299
window.localStorage.setItem(
sessionRefreshStorageKey(),
Date.now() + refreshDelay,
);

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.

P2: A full localStorage quota makes this write throw after the server has installed the cookie. The catch retries without resolving initialSessionExchange, leaving waiting uploads blocked despite a valid cookie. Treat the freshness hint as best-effort.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/reflex-base/src/reflex_base/.templates/web/utils/state.js, line 1296:

<comment>A full localStorage quota makes this write throw after the server has installed the cookie. The catch retries without resolving `initialSessionExchange`, leaving waiting uploads blocked despite a valid cookie. Treat the freshness hint as best-effort.</comment>

<file context>
@@ -1117,13 +1269,66 @@ export const connect = async (
+      setToken(session.client_token);
+      const refreshDelay =
+        (session.refresh_in ?? session.expires_in / 2) * 1000;
+      window.localStorage.setItem(
+        sessionRefreshStorageKey(),
+        Date.now() + refreshDelay,
</file context>
Suggested change
window.localStorage.setItem(
sessionRefreshStorageKey(),
Date.now() + refreshDelay,
);
try {
window.localStorage.setItem(
sessionRefreshStorageKey(),
Date.now() + refreshDelay,
);
} catch {
// The cookie exchange succeeded; the freshness hint is best-effort.
}

setItem: (key, value) => storage.set(key, value),
};
window.localStorage = {
getItem: () => (storedToken ? String(Date.now() + 3600000) : undefined),

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.

P3: This mock returns a fresh expiry for every localStorage key, so the warm-transport test still passes if state.js reads a misspelled or unscoped refresh key. Return the expiry only for reflex:ws://localhost:8000/_event:expires_at.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At tests/units/compiler/state_js.test.mjs, line 37:

<comment>This mock returns a fresh expiry for every localStorage key, so the warm-transport test still passes if `state.js` reads a misspelled or unscoped refresh key. Return the expiry only for `reflex:ws://localhost:8000/_event:expires_at`.</comment>

<file context>
@@ -27,11 +28,14 @@ async function setup({
     setItem: (key, value) => storage.set(key, value),
   };
+  window.localStorage = {
+    getItem: () => (storedToken ? String(Date.now() + 3600000) : undefined),
+  };
   const document = new EventTarget();
</file context>
Suggested change
getItem: () => (storedToken ? String(Date.now() + 3600000) : undefined),
getItem: (key) =>
key === "reflex:ws://localhost:8000/_event:expires_at" && storedToken
? String(Date.now() + 3600000)
: undefined,

@@ -0,0 +1 @@
Establish browser sessions over the WebSocket while hydration begins, then persist them through HTTP-only cookies with automatic refresh. Browser storage is isolated for each backend application. Idle tabs renew their session cookies automatically; transient cookie-exchange failures retry without blocking WebSocket events, and queued uploads remain cancellable.

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.

P3: This overstates isolation: the bootstrap still reads the shared legacy sessionStorage['token'] key, so an existing token can be reused across backend URLs. Limit this claim to the newly scoped keys.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/reflex-base/news/+session-cookie-bootstrap.feature.md, line 1:

<comment>This overstates isolation: the bootstrap still reads the shared legacy `sessionStorage['token']` key, so an existing token can be reused across backend URLs. Limit this claim to the newly scoped keys.</comment>

<file context>
@@ -0,0 +1 @@
+Establish browser sessions over the WebSocket while hydration begins, then persist them through HTTP-only cookies with automatic refresh. Browser storage is isolated for each backend application. Idle tabs renew their session cookies automatically; transient cookie-exchange failures retry without blocking WebSocket events, and queued uploads remain cancellable.
</file context>
Suggested change
Establish browser sessions over the WebSocket while hydration begins, then persist them through HTTP-only cookies with automatic refresh. Browser storage is isolated for each backend application. Idle tabs renew their session cookies automatically; transient cookie-exchange failures retry without blocking WebSocket events, and queued uploads remain cancellable.
Establish browser sessions over the WebSocket while hydration begins, then persist them through HTTP-only cookies with automatic refresh. New routing-token and freshness keys are scoped to each backend URL. Idle tabs renew their session cookies automatically; transient cookie-exchange failures retry without blocking WebSocket events, and queued uploads remain cancellable.

This branch has not been deployed

No deployments
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.

1 participant