odysseus-dev/odysseus · error · Error

statusText

Error message

statusText

What it means

Thrown when the fetch to GET /api/research/library returns a non-2xx status. The code throws new Error(res.statusText), so the message is whatever the server's status line carries. Note that over HTTP/2 (and many proxies) statusText is an empty string, so the surfaced message can literally be blank.

Solutions

  1. Inspect the actual status code (res.status) and response body in DevTools Network tab to identify why /api/research/library failed
  2. Include res.status in the thrown message so it is never blank: throw new Error(`HTTP ${res.status}`)
  3. If the server returns a JSON error body, parse it and prefer its detail/error field over statusText
  4. If 401/403, re-authenticate or reload the session before retrying the library load

Example fix

// before
if (!res.ok) throw new Error(res.statusText);

// after
if (!res.ok) {
  let msg = `HTTP ${res.status}`;
  try { const b = await res.json(); msg = b.detail || b.error || msg; } catch {}
  throw new Error(msg);
}
Defensive patterns

Strategy: try-catch

Try / catch

try { const res = await fetch(url, {credentials:'same-origin'}); if (!res.ok) { let m = `HTTP ${res.status}`; try { m = (await res.json()).detail || m; } catch {} throw new Error(m); } ... } catch (e) { grid.innerHTML = `<div class="hwfit-loading">Failed to load: ${_esc(e.message)}</div>`; }

Prevention

When it happens

Trigger: GET /api/research/library (optionally ?archived=true) responding with 4xx/5xx — e.g. 401 after session expiry, 500 if the research store fails to load, or a dev-server 404 when the API route is not mounted.

Common situations: Expired auth cookie on a long-open tab; backend restarted and lost state; running the static frontend against a server that does not expose the research API; HTTP/2 yielding empty statusText so users see 'Failed to load: ' with no reason.

Related errors


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/679c8ccdc78cf4d1. Report an issue: GitHub.

Appendix: source

Thrown at static/js/documentLibrary.js:2692

    let _researchSelectMode = false;
    let _researchArchivedView = false;
    const _researchSelected = new Set();

    async function _renderLibResearch() {
      const grid = document.getElementById('doclib-research-grid');
      const stats = document.getElementById('doclib-research-stats');
      if (!grid) return;
      // Show our whirlpool spinner instead of the plain "Loading..." text.
      grid.innerHTML = '';
      try {
        const _spm = (await import('./spinner.js')).default;
        const _sp = _spm.createWhirlpool(22);
        _sp.element.style.cssText = 'margin:18px auto;display:block;';
        grid.appendChild(_sp.element);
      } catch { grid.innerHTML = '<div class="hwfit-loading">Loading…</div>'; }
      try {
        const res = await fetch('/api/research/library' + (_researchArchivedView ? '?archived=true' : ''), { credentials: 'same-origin' });
        if (!res.ok) throw new Error(res.statusText);
        const data = await res.json();
        _researchItems = data.research || data || [];
      } catch (e) {
        grid.innerHTML = `<div class="hwfit-loading">Failed to load: ${_esc(e.message)}</div>`;
        return;
      }
      _renderResearchGrid();
    }

    // Toggle inline preview for a research row. Mirrors _toggleChatPreview
     // but pulls research-specific metadata: query, sources list (truncated),
     // followed by an "Open" action that loads the full report.
    async function _toggleResearchPreview(card, item) {
      const preview = card.querySelector('.doclib-chat-preview');
      if (!preview) return;
      const isOpen = card.classList.contains('doclib-card-expanded');
      const grid = card.closest('.doclib-grid');
      if (grid) {

View on GitHub (pinned to f9235ebbf1)