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

  1. Replace statusText with `HTTP ${res.status}` (plus parsed detail) so the message is never blank.
  2. Verify the session id exists (it comes from the library listing — refresh the library if stale).
  3. 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

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.