odysseus-dev/odysseus · error · Error

Failed to save signature

Error message

Failed to save signature

What it means

Thrown by _saveSignature in static/js/signature.js when POST {API_BASE}/api/signatures returns non-2xx. It reads the raw response text and throws that (or statusText as fallback), so the surfaced message is whatever the server sent — the caller wraps it as 'Failed to save signature'. The backend route lives at routes/signature_routes.py:101 and validates the payload (data URL, width, height, name).

Source

Thrown at static/js/signature.js:343

  return overlay;
}

async function _listSignatures() {
  const r = await fetch(`${API_BASE}/api/signatures`);
  if (!r.ok) return [];
  const data = await r.json();
  return data.signatures || [];
}

async function _saveSignature({ dataUrl, width, height, name }) {
  const r = await fetch(`${API_BASE}/api/signatures`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ data: dataUrl, width, height, name }),
  });
  if (!r.ok) {
    const t = await r.text();
    throw new Error(t || r.statusText);
  }
  return await r.json();
}

async function _deleteSignature(id) {
  await fetch(`${API_BASE}/api/signatures/${id}`, { method: 'DELETE' });
}

// Smoothness slider maps a single 0–10 value to the two main knobs.
// Persisted in localStorage so the user's preference sticks.
const SMOOTH_KEY = 'odysseus.signature.smoothness';
function _loadSmoothness() {
  const v = parseInt(localStorage.getItem(SMOOTH_KEY) || '', 10);
  return Number.isFinite(v) && v >= 0 && v <= 10 ? v : 7;
}
function _saveSmoothness(v) {
  try { localStorage.setItem(SMOOTH_KEY, String(v)); } catch (_) {}
}

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Read the thrown text — it is the server's validation message (FastAPI 422 detail) and names the exact bad field.
  2. Confirm the payload types: data must be a data:image/... URL string, width/height numbers, name a non-empty string.
  3. If 413, shrink the canvas export (lower resolution or JPEG/PNG compression level) before sending.
  4. If 401, re-authenticate; the signature canvas state stays in memory so retry after login works.

Example fix

// before
if (!r.ok) {
  const t = await r.text();
  throw new Error(t || r.statusText);
}
// after — parse FastAPI detail for a cleaner message
if (!r.ok) {
  const t = await r.text();
  let msg = t || r.statusText;
  try { msg = JSON.parse(t).detail || msg; } catch (_) {}
  throw new Error(`${msg} (HTTP ${r.status})`);
}
Defensive patterns

Strategy: validation

Validate before calling

if (!/^data:image\/png;base64,/.test(dataUrl)) throw new Error('Invalid signature data URL');
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) throw new Error('Invalid dimensions');
if (!name || !name.trim()) throw new Error('Name required');
if (dataUrl.length > 2_000_000) throw new Error('Signature image too large');

Type guard

/** @returns {boolean} payload is shaped for POST /api/signatures */
function isValidSignaturePayload(p) {
  return typeof p?.dataUrl === 'string' && p.dataUrl.startsWith('data:image/')
    && Number.isFinite(p?.width) && p?.width > 0
    && Number.isFinite(p?.height) && p?.height > 0
    && typeof p?.name === 'string' && p.name.trim().length > 0;
}

Try / catch

try { await _saveSignature(payload); } catch (err) { showError(err.message || 'Failed to save signature'); // keep the drawn canvas so the user can retry }

Prevention

When it happens

Trigger: Saving a drawn signature: POST /api/signatures with {data: dataUrl, width, height, name}. Fails with 422 when the data URL is malformed or width/height/name missing/invalid, 401/403 when unauthenticated (the route is behind the auth gate), 413 when the data URL exceeds the server's body limit, 500 on storage errors.

Common situations: Huge canvas exports producing multi-MB base64 data URLs rejected by a body-size limit; empty name; width/height sent as strings instead of numbers after a schema change; session expired before save.

Related errors


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