Auto-retry once after CSRF token resync in lt.api (#86)
lt.api's fetch wrapper (_apiFetchAuth in base.js — the live implementation lt.api.* resolves to) already resynced window.CSRF_TOKEN from a 403 response's csrf_token field, but still threw immediately — every caller saw a raw "Invalid CSRF token" error on the FIRST attempt, with no transparent retry. Since CsrfMiddleware's token lifetime (1h) is shorter than the session idle timeout (5h), this was a routine, fully recoverable case (an hour of page inactivity, or a write in another tab rotating the shared token), not a real rejection. After resyncing the token from a 403 body that carries one, now retries the original request exactly once with the fresh token before surfacing an error — transparent to the caller on the common case, with a `retried` flag preventing more than one retry so a genuinely broken session still fails cleanly instead of looping. Verified via jsdom with a mocked fetch: a 403-then-succeeds sequence resolves successfully with exactly 2 network calls and the correct final token; a persistently-403 sequence still throws after exactly 2 calls (no infinite retry). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lhz7pGMaoTfL5sdYS5XiKv
This commit is contained in:
+10
-1
@@ -2801,7 +2801,7 @@
|
||||
};
|
||||
|
||||
// Patch lt.api — auth-aware wrapper (renamed to avoid strict-mode duplicate declaration)
|
||||
async function _apiFetchAuth(method, url, body) {
|
||||
async function _apiFetchAuth(method, url, body, retried) {
|
||||
if (_authAccess && auth.isExpiringSoon()) await auth.refresh();
|
||||
const opts = { method, headers: Object.assign({ 'Content-Type': 'application/json' }, csrfHeaders()) };
|
||||
if (_authAccess) opts.headers['Authorization'] = 'Bearer ' + _authAccess;
|
||||
@@ -2821,6 +2821,15 @@
|
||||
// Resync CSRF token from any response body that carries a fresh one
|
||||
// (bootstrap rotates on success and returns the current token on rejection).
|
||||
if (data && data.csrf_token) global.CSRF_TOKEN = data.csrf_token;
|
||||
// Auto-retry once on a stale-CSRF-token 403: the token lifetime (1h) is
|
||||
// shorter than the session idle timeout (5h), so this is a routine,
|
||||
// recoverable case (an hour of inactivity, or a write in another tab
|
||||
// rotating the shared token) rather than a real rejection — resyncing
|
||||
// above already has the fresh token, so silently resending once succeeds
|
||||
// transparently instead of surfacing a confusing error on the first try.
|
||||
if (resp.status === 403 && !retried && data && data.csrf_token) {
|
||||
return _apiFetchAuth(method, url, body, true);
|
||||
}
|
||||
if (!resp.ok) {
|
||||
const err = new Error(data.error || data.message || 'HTTP ' + resp.status);
|
||||
err.data = data;
|
||||
|
||||
Reference in New Issue
Block a user