This commit is contained in:
doodee123 2026-08-04 02:40:42 +02:00 committed by GitHub
commit c748351027
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 49 additions and 2 deletions

31
CHANGELOG.md Normal file
View file

@ -0,0 +1,31 @@
# Changelog
All notable changes to Odysseus will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased]
### Fixed
- **auth: prevent spurious login redirect on mobile during text input**
The global `window.fetch` 401 interceptor (`static/app.js`) unconditionally
redirected to `/login` on any background API call that returned 401. On mobile,
when the user backgrounded the app (switched apps, locked phone), session
cookies could become stale. When they returned, background fetches (task
notification polling every 30s, calendar refetch on tab resume, notes badge
refresh) received 401 responses and triggered an immediate redirect to
`/login`, which wiped the user's unsent text input and chat state.
This was especially noticeable on mobile browsers which aggressively suspend
background tabs and expire cookies faster than desktop browsers.
The fix adds three improvements to the 401 interceptor:
1. **Retry once after 2s** — catches transient 401s from stale cookies on
mobile tab resume that resolve on retry.
2. **Deduplication** — prevents multiple stacked redirects from concurrent
background fetches (e.g. task polling + calendar refetch simultaneously
hitting 401).
3. **Typing guard** — if the user is actively typing in the message input
when the redirect fires, it defers until the input loses focus, preserving
the draft.

View file

@ -185,11 +185,27 @@ function initRailHoverLabels() {
});
}
// Redirect to login on 401 from any fetch
// Redirect to login on 401 from any fetch — but retry once first to avoid
// spurious redirects on mobile when background tabs resume after cookie staleness.
// If the user is mid-typing, delay the redirect so their draft isn't wiped.
let _401Cooldown = false;
const _origFetch = window.fetch;
window.fetch = async function(...args) {
const res = await _origFetch.apply(this, args);
if (res.status === 401 && !String(args[0]).includes('/api/auth/')) {
if (res.status !== 401 || String(args[0]).includes('/api/auth/')) return res;
if (_401Cooldown) return res;
_401Cooldown = true;
await new Promise(r => setTimeout(r, 2000));
try {
const retry = await _origFetch.apply(this, args);
if (retry.status !== 401) { _401Cooldown = false; return res; }
} catch (_) {}
const msgInput = document.getElementById('message');
const isTyping = msgInput && msgInput === document.activeElement && msgInput.value.trim().length > 0;
if (isTyping) {
const _onBlur = () => { msgInput.removeEventListener('blur', _onBlur); window.location.href = '/login'; };
msgInput.addEventListener('blur', _onBlur);
} else {
window.location.href = '/login';
}
return res;