odysseus-dev/odysseus · error · Error
HTTP ${resp.status}${detail ? `: ${detail}` : ''}
Error message
HTTP ${resp.status}${detail ? `: ${detail}` : ''} What it means
Thrown by the 'Save over original' flow in galleryEditor.js when POST /api/gallery/{imageId}/replace fails. The client tries to parse the error body for detail/error and appends it to the status code. The surrounding catch specifically recognizes network TypeErrors and suggests 'Save as copy', so this message is for genuine HTTP failures (4xx/5xx).
Source
Thrown at static/js/galleryEditor.js:3306
const flat = flatten();
const ext = (state.originalExt || 'png').toLowerCase();
const isJpeg = ext === 'jpg' || ext === 'jpeg';
const mime = isJpeg ? 'image/jpeg' : 'image/png';
const quality = isJpeg ? 0.92 : undefined;
blob = await new Promise((resolve, reject) => {
flat.toBlob(b => b ? resolve(b) : reject(new Error('Canvas encode failed')), mime, quality);
});
const fd = new FormData();
fd.append('image', blob, `edited.${isJpeg ? 'jpg' : 'png'}`);
const resp = await fetch(`${API_BASE}/api/gallery/${state.imageId}/replace`, {
method: 'POST',
credentials: 'same-origin',
body: fd,
});
if (!resp.ok) {
let detail = '';
try { const j = await resp.json(); detail = j.detail || j.error || ''; } catch {}
throw new Error(`HTTP ${resp.status}${detail ? `: ${detail}` : ''}`);
}
const totalMs = Math.round(performance.now() - t0);
if (uiModule) uiModule.showToast(`Saved over original (${(blob.size / 1024 / 1024).toFixed(1)}MB · ${(totalMs / 1000).toFixed(1)}s)`, 4000);
window.dispatchEvent(new CustomEvent('gallery-refresh'));
savedOk = true;
} catch (e) {
console.error('[save] error:', e);
const sizeMB = blob ? ` (${(blob.size / 1024 / 1024).toFixed(1)}MB)` : '';
let msg = e?.message || 'unknown';
if (e?.name === 'TypeError' || /fetch|network|load failed/i.test(msg)) {
msg = `network dropped${sizeMB} — try "Save as copy" or check connection`;
} else {
msg += sizeMB;
}
if (uiModule) uiModule.showToast('Failed to save: ' + msg, 6000);
} finally {
endBusy();
if (savedOk) _flashSaveButtonOk();View on GitHub (pinned to f9235ebbf1)
Solutions
- Use 'Save as copy' (the sibling flow) to confirm the upload path itself works.
- If 413, raise the proxy's body-size limit or save as JPEG at slightly lower quality to shrink the blob.
- Verify the original file still exists at the path the server expects and is writable.
- Check server logs for the detail string; re-authenticate if 401.
Defensive patterns
Strategy: fallback
Validate before calling
const blob = await canvasToBlob(flat, mime, quality);
if (blob.size > PROXY_LIMIT) { quality *= 0.8; /* re-encode */ } Try / catch
try { ... } catch (e) { if (/fetch|network/i.test(e.message)) msg = 'network dropped — try Save as copy'; if (is413) retry with lower quality; offer 'Save as copy' as fallback path; } Prevention
- Cap export size below the proxy's body limit
- Keep the draft saved so a failed replace does not lose edits
- Offer the copy path automatically when replace fails
When it happens
Trigger: Replacing an original photo when the file on disk is read-only or missing (404/500); payload exceeds a reverse proxy's client_max_body_size (413) for multi-MB PNGs; session cookie expired (401); server-side storage full.
Common situations: Editing large photos saved as high-quality PNG (tens of MB) behind nginx/caddy defaults; photo library moved or on removable media that is unmounted; permissions changed after restore.
Related errors
- HTTP ${saveRes.status}: ${errBody.substring(0, 120)}
- Not your image
- No image provided
- statusText
- Server returned ${res.status}
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/eb93d8e944643f93.
Report an issue: GitHub.