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

  1. Confirm the image/mask backend service is running and reachable from the app server.
  2. Check the response detail/error text — it names the missing model or config (e.g. SAM weights not found).
  3. If the image is very large, downscale the layer or raise the server's request body limit before retrying.
  4. 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

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


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