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
- Read the thrown text — it is the server's validation message (FastAPI 422 detail) and names the exact bad field.
- Confirm the payload types: data must be a data:image/... URL string, width/height numbers, name a non-empty string.
- If 413, shrink the canvas export (lower resolution or JPEG/PNG compression level) before sending.
- 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
- Validate data-URL prefix, positive numeric dimensions, and non-empty name before POSTing.
- Cap the exported canvas size (scale or compress) to stay under body limits.
- Parse the FastAPI 422 detail out of the response text for field-level hints.
- Do not clear the signature canvas until save succeeds.
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.