odysseus-dev/odysseus · error · Error

(data && (data.error || data.detail)) || `HTTP ${res.status}

Error message

(data && (data.error || data.detail)) || `HTTP ${res.status}`

What it means

Thrown by the markdown image upload path in static/js/document.js when POST /api/upload (multipart FormData of one or more 'files') returns non-2xx. The message prefers data.error/data.detail and falls back to `HTTP <status>`; because res.json() is already wrapped in try/catch, a non-JSON body yields the plain status string.

Source

Thrown at static/js/document.js:3571

      if (uiModule) uiModule.showError('Choose an image file');
      return;
    }
    if (_activeDocLanguage() !== 'markdown') {
      if (uiModule) uiModule.showError('Switch the document to markdown before inserting images');
      return;
    }

    const fd = new FormData();
    images.forEach(file => fd.append('files', file));
    try {
      const res = await fetch(`${API_BASE}/api/upload`, {
        method: 'POST',
        credentials: 'same-origin',
        body: fd,
      });
      let data = null;
      try { data = await res.json(); } catch (_) {}
      if (!res.ok) throw new Error((data && (data.error || data.detail)) || `HTTP ${res.status}`);
      const uploaded = Array.isArray(data?.files) ? data.files : [];
      if (!uploaded.length) throw new Error('No uploaded files returned');
      _insertMarkdownImages(uploaded);
      if (uiModule) uiModule.showToast(images.length === 1 ? 'Image inserted' : 'Images inserted');
    } catch (err) {
      console.error('Failed to insert markdown image:', err);
      if (uiModule) uiModule.showError('Failed to insert image');
    }
  }

  async function _handleMarkdownImageUpload(e) {
    const files = e.target.files;
    e.target.value = '';
    await _uploadMarkdownImages(files);
  }

  function _renderComposeAttachments() {
    const container = document.getElementById('doc-email-compose-atts');

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Check the exact status: 413 means raise the upload size limit (e.g. nginx client_max_body_size or the backend's multipart cap).
  2. Verify the field name is 'files' and Content-Type is unset (browser sets multipart boundary) — do not set the JSON header for FormData.
  3. Confirm the server's upload directory exists and is writable.
  4. Compress images client-side before upload if large pastes are common.

Example fix

// before
const res = await fetch(`${API_BASE}/api/upload`, { method: 'POST', credentials: 'same-origin', body: fd });

// after — surface the server's reason in the toast
const res = await fetch(`${API_BASE}/api/upload`, { method: 'POST', credentials: 'same-origin', body: fd });
if (!res.ok) {
  let reason = `HTTP ${res.status}`;
  try { const j = await res.json(); reason = j?.error || j?.detail || reason; } catch (_) {}
  throw new Error(`Upload failed: ${reason}`);
}
Defensive patterns

Strategy: try-catch

Validate before calling

const okTypes = ['image/png', 'image/jpeg', 'image/gif', 'image/webp', 'image/svg+xml'];
const images = Array.from(files || []).filter(f => okTypes.includes(f.type) && f.size > 0 && f.size <= MAX_BYTES);

Type guard

function isUploadResult(data) {
  return data != null && typeof data === 'object' && Array.isArray(data.files) && data.files.length > 0;
}

Try / catch

try {
  const res = await fetch(`${API_BASE}/api/upload`, { method: 'POST', credentials: 'same-origin', body: fd });
  let data = null;
  try { data = await res.json(); } catch (_) {}
  if (!res.ok) throw new Error((data && (data.error || data.detail)) || `HTTP ${res.status}`);
  if (!isUploadResult(data)) throw new Error('No uploaded files returned');
  _insertMarkdownImages(data.files);
} catch (err) {
  if (uiModule) uiModule.showError(`Failed to insert image: ${err.message}`);
}

Prevention

When it happens

Trigger: Pasting/dragging images into a markdown document when the upload exceeds the server's max body size (413), an unsupported file type is rejected (400), the uploads directory is unwritable (500), or auth fails (401).

Common situations: Screenshot pastes that are several MB hitting a 1 MB default proxy limit (client_max_body_size); nginx 502 during upload; disk full on the server.

Related errors


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