odysseus-dev/odysseus · warning · Error
res.statusText
Error message
res.statusText
What it means
Thrown by _copyChatById in documentLibrary.js when GET /api/history/<sessionId> returns non-2xx while serializing a chat transcript to the clipboard. It uses res.statusText as the message — a field that is frequently empty (and is always empty over HTTP/2), producing a contextless 'Failed to copy' style error downstream.
Source
Thrown at static/js/documentLibrary.js:134
siblings.forEach(s => { s.style.opacity = '0'; });
requestAnimationFrame(() => {
siblings.forEach(s => {
s.style.transition = 'opacity 0.15s ease';
s.style.opacity = '1';
});
setTimeout(() => { siblings.forEach(s => { s.style.transition = ''; s.style.opacity = ''; }); }, 200);
});
}
}
// Fetch a chat's full history and serialize as plain-text transcript,
// then write to the clipboard. Same User: / Assistant: format the chat
// header's "Copy Chat" button uses, but works for any session ID — the
// library doesn't need the chat to be loaded in the UI first.
async function _copyChatById(sessionId) {
try {
const res = await fetch(`${API_BASE}/api/history/${sessionId}`, { credentials: 'same-origin' });
if (!res.ok) throw new Error(res.statusText);
const data = await res.json();
const history = Array.isArray(data) ? data : (data.history || []);
const lines = [];
for (const m of history) {
if (m.role !== 'user' && m.role !== 'assistant') continue;
const label = m.role === 'user' ? 'User' : 'Assistant';
const body = (m.content || '')
.replace(/<think>[\s\S]*?<\/think>/g, '')
.replace(/<think>[\s\S]*$/, '')
.trim();
if (body) lines.push(`${label}: ${body}`);
}
const text = lines.join('\n\n');
if (uiModule && uiModule.copyToClipboard) {
await uiModule.copyToClipboard(text);
} else {
await navigator.clipboard.writeText(text);
}View on GitHub (pinned to f9235ebbf1)
Solutions
- Replace statusText with `HTTP ${res.status}` (plus parsed detail) so the message is never blank.
- Verify the session id exists (it comes from the library listing — refresh the library if stale).
- Confirm API_BASE points at the origin that serves /api/history.
Example fix
// before
const res = await fetch(`${API_BASE}/api/history/${sessionId}`, { credentials: 'same-origin' });
if (!res.ok) throw new Error(res.statusText);
// after
const res = await fetch(`${API_BASE}/api/history/${sessionId}`, { credentials: 'same-origin' });
if (!res.ok) {
let detail = '';
try { const j = await res.json(); detail = j?.detail || ''; } catch (_) {}
throw new Error(`HTTP ${res.status}${detail ? ` — ${detail}` : ''}`);
} Defensive patterns
Strategy: try-catch
Validate before calling
if (!sessionId) return; // nothing to copy
Type guard
function isHistoryPayload(data) {
if (Array.isArray(data)) return true;
return data != null && typeof data === 'object' && Array.isArray(data.history);
} Try / catch
try {
const res = await fetch(`${API_BASE}/api/history/${sessionId}`, { credentials: 'same-origin' });
if (!res.ok) throw new Error(`HTTP ${res.status}`); // statusText is empty on HTTP/2
const data = await res.json();
if (!isHistoryPayload(data)) throw new Error('Unexpected history payload');
} catch (e) {
if (uiModule) uiModule.showError(`Could not copy chat: ${e.message}`);
} Prevention
- Never rely on res.statusText for user-facing messages — it is empty over HTTP/2 and often stripped by proxies.
- Use `HTTP ${res.status}` plus parsed detail instead.
- Verify API_BASE and session freshness before issuing history fetches.
When it happens
Trigger: Copying a chat whose session id no longer exists server-side (404); expired session (401); wrong/undeclared API_BASE origin causing CORS or 404; proxy stripping reason phrases so statusText is ''.
Common situations: Copying old chats after server data was reset; HTTP/2 or h2c proxied deployments where statusText is always ''; typo'd session id in the card's data attribute.
Related errors
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/229e6fae6be4d5c8.
Report an issue: GitHub.