odysseus-dev/odysseus · error · Error
data.detail || data.error || `Mask failed (${res.status})`
Error message
data.detail || data.error || `Mask failed (${res.status})` What it means
Thrown by _requestAndApplySamMask in galleryEditor.js when POST /api/image/mask either returns a non-ok status or a 2xx body without a mask field. The message prefers the server's detail (FastAPI style) or error field, falling back to 'Mask failed (status)'. A json parse failure is swallowed to {} so HTML error pages degrade to the status form.
Source
Thrown at static/js/galleryEditor.js:2008
btn.disabled = false;
btn.innerHTML = old || '<span class="ge-btn-ai-mark" aria-hidden="true">✦</span>Find';
}
}
}
async function _requestAndApplySamMask(layer, payload, mode, seedPoint, opts = {}) {
const res = await fetch('/api/image/mask', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
signal: opts.signal,
body: JSON.stringify({
image: layer.canvas.toDataURL('image/png').split(',')[1],
...payload,
}),
});
const data = await res.json().catch(() => ({}));
if (!res.ok || !data.mask) {
throw new Error(data.detail || data.error || `Mask failed (${res.status})`);
}
if (!data.bbox) {
throw new Error(data.grounding ? `Found ${data.grounding.label || 'object'}, but SAM returned an empty mask` : 'SAM returned an empty mask');
}
const img = new Image();
await new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = () => reject(new Error('Failed to decode mask'));
img.src = 'data:image/png;base64,' + data.mask;
});
const mask = document.createElement('canvas');
mask.width = layer.canvas.width;
mask.height = layer.canvas.height;
const mctx = mask.getContext('2d');
mctx.drawImage(img, 0, 0, mask.width, mask.height);
const maskData = mctx.getImageData(0, 0, mask.width, mask.height);
const md = maskData.data;View on GitHub (pinned to f9235ebbf1)
Solutions
- Confirm the image/mask backend service is running and reachable from the app server.
- Check the response detail/error text — it names the missing model or config (e.g. SAM weights not found).
- If the image is very large, downscale the layer or raise the server's request body limit before retrying.
- Reinstall/download the SAM model assets the endpoint reports as missing.
Defensive patterns
Strategy: try-catch
Validate before calling
if (!layer?.canvas) throw new Error('No active layer to mask');
const dataUrl = layer.canvas.toDataURL('image/png');
if (dataUrl.length > 8_000_000) throw new Error('Image too large for mask request — downscale first'); Type guard
const isMaskResponse = (d) => d && typeof d.mask === 'string' && (Array.isArray(d.bbox) || d.bbox === undefined);
Try / catch
try { const data = await res.json().catch(() => ({})); if (!res.ok || !data.mask) throw new Error(data.detail || data.error || `Mask failed (${res.status})`); } catch (e) { if (e.name === 'AbortError') return; showError(e.message); } Prevention
- Downscale oversized canvases before base64-encoding the payload
- Pass opts.signal and special-case AbortError so cancellations are not shown as failures
- Health-check the mask service before enabling the AI select tools
When it happens
Trigger: Clicking an AI select/mask tool (SAM) in the photo editor when the backend grounding/DINO endpoint is not configured; the SAM service is down or unlicensed; the request body's base64 image is rejected (too large, malformed); opts.signal aborted mid-request.
Common situations: Editor deployed without the SAM/Grounding service running; model weights missing after an update; a very high-resolution layer whose dataURL exceeds the server's body limit (413); aborted requests when the user cancels mid-inference surface as this generic error.
Related errors
- Found ${data.grounding.label || 'object'}, but SAM returned
- statusText
- Server returned ${res.status}
- HTTP ${resp.status}${detail ? `: ${detail}` : ''}
- HTTP ${saveRes.status}: ${errBody.substring(0, 120)}
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/d0fa22f20587d103.
Report an issue: GitHub.