{"record":{"id":"3e858608110c9dde","repo":"odysseus-dev/odysseus","slug":"failed-to-update-task","errorCode":null,"errorMessage":"Failed to update task","messagePattern":"Failed to update task","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"static/js/tasks.js","lineNumber":90,"sourceCode":"async function _createTask(data) {\n  const res = await fetch(`${API_BASE}/api/tasks`, {\n    method: 'POST',\n    credentials: 'same-origin',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify(data),\n  });\n  if (!res.ok) throw new Error('Failed to create task');\n  return await res.json();\n}\n\nasync function _updateTask(id, data) {\n  const res = await fetch(`${API_BASE}/api/tasks/${id}`, {\n    method: 'PUT',\n    credentials: 'same-origin',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify(data),\n  });\n  if (!res.ok) throw new Error('Failed to update task');\n  return await res.json();\n}\n\nasync function _deleteTask(id) {\n  const res = await fetch(`${API_BASE}/api/tasks/${id}`, {\n    method: 'DELETE', credentials: 'same-origin',\n  });\n  if (!res.ok) throw new Error('Failed to delete task');\n}\n\nfunction _taskCardById(id) {\n  const safe = (window.CSS && CSS.escape) ? CSS.escape(String(id)) : String(id).replace(/\"/g, '\\\\\"');\n  return document.querySelector(`.task-card[data-id=\"${safe}\"]`);\n}\n\nfunction _animateTaskRemoval(ids) {\n  const cards = ids.map(_taskCardById).filter(Boolean);\n  if (!cards.length) return Promise.resolve();","sourceCodeStart":72,"sourceCodeEnd":108,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/static/js/tasks.js#L72-L108","documentation":"Thrown by _updateTask (static/js/tasks.js:90) when PUT /api/tasks/{id} returns non-2xx; message is the generic 'Failed to update task' with no status. Tests (test_task_cookbook_admin_gate.py, test_task_chain_owner_scope.py) show this route enforces ownership and admin gating, so 403 is a first-class outcome, not an anomaly.","triggerScenarios":"Editing/saving a task card: PUT the task JSON. 404 when the task id no longer exists (deleted elsewhere), 403 when the user does not own the task or lacks rights, 422 on invalid field values, 401 on expired session.","commonSituations":"Task deleted in another tab or by the agent; non-owner trying to edit a shared instance; stale page holding an id that was recreated; schedule/action validation same as create.","solutions":["Check status: 403 → you are not the owner, refresh to see current state; 404 → task gone, drop the card and reload; 422 → fix the flagged field.","Re-login on 401.","Include the status and server detail in the thrown message for diagnosability.","Reload the task list after any 404/403 instead of retrying blindly."],"exampleFix":"// before\nif (!res.ok) throw new Error('Failed to update task');\n// after\nconst d = await res.json().catch(() => ({}));\nif (!res.ok) throw new Error(d.detail || `Failed to update task (HTTP ${res.status})`);","handlingStrategy":"try-catch","validationCode":"if (!id) { throw new Error('Task id required'); }","typeGuard":null,"tryCatchPattern":"try { await _updateTask(id, data); } catch (err) { if (/HTTP 404|Failed to update/.test(err.message)) await refreshTasks(); showError(err.message); }","preventionTips":["Refresh the task list after 404/403 instead of retrying the same PUT.","Only edit tasks you own — the route enforces ownership server-side.","Add status + detail to the error message.","Resolve 422s by reading the FastAPI validation body."],"tags":["fetch","api","http","tasks","authorization"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}