{"record":{"id":"229e6fae6be4d5c8","repo":"odysseus-dev/odysseus","slug":"res-statustext","errorCode":null,"errorMessage":"res.statusText","messagePattern":"res\\.statusText","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"static/js/documentLibrary.js","lineNumber":134,"sourceCode":"      siblings.forEach(s => { s.style.opacity = '0'; });\n      requestAnimationFrame(() => {\n        siblings.forEach(s => {\n          s.style.transition = 'opacity 0.15s ease';\n          s.style.opacity = '1';\n        });\n        setTimeout(() => { siblings.forEach(s => { s.style.transition = ''; s.style.opacity = ''; }); }, 200);\n      });\n    }\n  }\n\n  // Fetch a chat's full history and serialize as plain-text transcript,\n  // then write to the clipboard. Same User: / Assistant: format the chat\n  // header's \"Copy Chat\" button uses, but works for any session ID — the\n  // library doesn't need the chat to be loaded in the UI first.\n  async function _copyChatById(sessionId) {\n    try {\n      const res = await fetch(`${API_BASE}/api/history/${sessionId}`, { credentials: 'same-origin' });\n      if (!res.ok) throw new Error(res.statusText);\n      const data = await res.json();\n      const history = Array.isArray(data) ? data : (data.history || []);\n      const lines = [];\n      for (const m of history) {\n        if (m.role !== 'user' && m.role !== 'assistant') continue;\n        const label = m.role === 'user' ? 'User' : 'Assistant';\n        const body = (m.content || '')\n          .replace(/<think>[\\s\\S]*?<\\/think>/g, '')\n          .replace(/<think>[\\s\\S]*$/, '')\n          .trim();\n        if (body) lines.push(`${label}: ${body}`);\n      }\n      const text = lines.join('\\n\\n');\n      if (uiModule && uiModule.copyToClipboard) {\n        await uiModule.copyToClipboard(text);\n      } else {\n        await navigator.clipboard.writeText(text);\n      }","sourceCodeStart":116,"sourceCodeEnd":152,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/static/js/documentLibrary.js#L116-L152","documentation":"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.","triggerScenarios":"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 ''.","commonSituations":"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.","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."],"exampleFix":"// before\nconst res = await fetch(`${API_BASE}/api/history/${sessionId}`, { credentials: 'same-origin' });\nif (!res.ok) throw new Error(res.statusText);\n\n// after\nconst res = await fetch(`${API_BASE}/api/history/${sessionId}`, { credentials: 'same-origin' });\nif (!res.ok) {\n  let detail = '';\n  try { const j = await res.json(); detail = j?.detail || ''; } catch (_) {}\n  throw new Error(`HTTP ${res.status}${detail ? ` — ${detail}` : ''}`);\n}","handlingStrategy":"try-catch","validationCode":"if (!sessionId) return; // nothing to copy","typeGuard":"function isHistoryPayload(data) {\n  if (Array.isArray(data)) return true;\n  return data != null && typeof data === 'object' && Array.isArray(data.history);\n}","tryCatchPattern":"try {\n  const res = await fetch(`${API_BASE}/api/history/${sessionId}`, { credentials: 'same-origin' });\n  if (!res.ok) throw new Error(`HTTP ${res.status}`); // statusText is empty on HTTP/2\n  const data = await res.json();\n  if (!isHistoryPayload(data)) throw new Error('Unexpected history payload');\n} catch (e) {\n  if (uiModule) uiModule.showError(`Could not copy chat: ${e.message}`);\n}","preventionTips":["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."],"tags":["http","statustext","clipboard","chat-history","poor-error-message"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}