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

  1. Use 'Save as copy' (the sibling flow) to confirm the upload path itself works.
  2. If 413, raise the proxy's body-size limit or save as JPEG at slightly lower quality to shrink the blob.
  3. Verify the original file still exists at the path the server expects and is writable.
  4. 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

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


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