{"record":{"id":"83a2fb0e3ee5654f","repo":"odysseus-dev/odysseus","slug":"failed-to-save-signature","errorCode":null,"errorMessage":"Failed to save signature","messagePattern":"Failed to save signature","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"static/js/signature.js","lineNumber":343,"sourceCode":"  return overlay;\n}\n\nasync function _listSignatures() {\n  const r = await fetch(`${API_BASE}/api/signatures`);\n  if (!r.ok) return [];\n  const data = await r.json();\n  return data.signatures || [];\n}\n\nasync function _saveSignature({ dataUrl, width, height, name }) {\n  const r = await fetch(`${API_BASE}/api/signatures`, {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify({ data: dataUrl, width, height, name }),\n  });\n  if (!r.ok) {\n    const t = await r.text();\n    throw new Error(t || r.statusText);\n  }\n  return await r.json();\n}\n\nasync function _deleteSignature(id) {\n  await fetch(`${API_BASE}/api/signatures/${id}`, { method: 'DELETE' });\n}\n\n// Smoothness slider maps a single 0–10 value to the two main knobs.\n// Persisted in localStorage so the user's preference sticks.\nconst SMOOTH_KEY = 'odysseus.signature.smoothness';\nfunction _loadSmoothness() {\n  const v = parseInt(localStorage.getItem(SMOOTH_KEY) || '', 10);\n  return Number.isFinite(v) && v >= 0 && v <= 10 ? v : 7;\n}\nfunction _saveSmoothness(v) {\n  try { localStorage.setItem(SMOOTH_KEY, String(v)); } catch (_) {}\n}","sourceCodeStart":325,"sourceCodeEnd":361,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/static/js/signature.js#L325-L361","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nif (!r.ok) {\n  const t = await r.text();\n  throw new Error(t || r.statusText);\n}\n// after — parse FastAPI detail for a cleaner message\nif (!r.ok) {\n  const t = await r.text();\n  let msg = t || r.statusText;\n  try { msg = JSON.parse(t).detail || msg; } catch (_) {}\n  throw new Error(`${msg} (HTTP ${r.status})`);\n}","handlingStrategy":"validation","validationCode":"if (!/^data:image\\/png;base64,/.test(dataUrl)) throw new Error('Invalid signature data URL');\nif (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) throw new Error('Invalid dimensions');\nif (!name || !name.trim()) throw new Error('Name required');\nif (dataUrl.length > 2_000_000) throw new Error('Signature image too large');","typeGuard":"/** @returns {boolean} payload is shaped for POST /api/signatures */\nfunction isValidSignaturePayload(p) {\n  return typeof p?.dataUrl === 'string' && p.dataUrl.startsWith('data:image/')\n    && Number.isFinite(p?.width) && p?.width > 0\n    && Number.isFinite(p?.height) && p?.height > 0\n    && typeof p?.name === 'string' && p.name.trim().length > 0;\n}","tryCatchPattern":"try { await _saveSignature(payload); } catch (err) { showError(err.message || 'Failed to save signature'); // keep the drawn canvas so the user can retry }","preventionTips":["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."],"tags":["fetch","api","http","signature","validation"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}