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
- Check the exact status: 413 means raise the upload size limit (e.g. nginx client_max_body_size or the backend's multipart cap).
- Verify the field name is 'files' and Content-Type is unset (browser sets multipart boundary) — do not set the JSON header for FormData.
- Confirm the server's upload directory exists and is writable.
- 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
- Never set Content-Type manually on FormData requests; let the browser set the multipart boundary.
- Enforce a client-side size cap matching the server's limit before uploading.
- Compress/downscale pasted screenshots before upload.
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
- data?.error || data?.detail || `HTTP ${res.status}`
- Document create failed: HTTP ${res.status}
- Document save failed: HTTP ${res.status}
- PDF import failed
- HTTP ${res.status}
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/15334cc88faa92b9.
Report an issue: GitHub.