{"record":{"id":"8b08538eb09d6ded","repo":"odysseus-dev/odysseus","slug":"failed-to-stop-task-res-status","errorCode":null,"errorMessage":"Failed to stop task (${res.status})","messagePattern":"Failed to stop task \\((.+?)\\)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"static/js/tasks.js","lineNumber":178,"sourceCode":"      if (data && data.detail) msg = data.detail;\n    } catch (_) {}\n    if (res.status === 409) msg = 'Task is already running';\n    throw new Error(msg);\n  }\n}\n\nasync function _stopTask(id) {\n  const res = await fetch(`${API_BASE}/api/tasks/${id}/stop`, {\n    method: 'POST',\n    credentials: 'same-origin',\n  });\n  if (!res.ok) {\n    let msg = `Failed to stop task (${res.status})`;\n    try {\n      const data = await res.json();\n      if (data && data.detail) msg = data.detail;\n    } catch (_) {}\n    throw new Error(msg);\n  }\n}\n\nasync function _fetchRuns(taskId, limit = 10) {\n  const res = await fetch(`${API_BASE}/api/tasks/${taskId}/runs?limit=${limit}`, {\n    credentials: 'same-origin',\n  });\n  if (!res.ok) return [];\n  const data = await res.json();\n  return data.runs || [];\n}\n\nlet _outputTargets = null;\nasync function _fetchOutputTargets() {\n  if (_outputTargets) return _outputTargets;\n  try {\n    const res = await fetch(`${API_BASE}/api/tasks/meta/output-targets`, { credentials: 'same-origin' });\n    const data = await res.json();","sourceCodeStart":160,"sourceCodeEnd":196,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/static/js/tasks.js#L160-L196","documentation":"Thrown by the tasks UI when POST /api/tasks/{id}/stop returns a non-2xx status. As with trigger, the client prefers the backend's JSON `detail` field; this literal is the fallback message. There is no special-casing for stop, so a 409 (not running) surfaces as the backend detail if present, otherwise the generic status message.","triggerScenarios":"Clicking Stop on a task that is not currently running, on a task id that no longer exists (404), with an expired session (401/403), or when the stop endpoint returns a non-JSON body so data.detail cannot be read.","commonSituations":"Stop button clicked from a stale UI where the run already finished; race between the run completing and the user clicking stop; auth cookie expired.","solutions":["Check the HTTP status: 404 = task gone (refresh list), 401/403 = re-authenticate, 409/400 = run already finished (harmless, refresh the run state)","Refresh the task's status via GET /api/tasks before showing the stop control","Inspect the server log if status is 500"],"exampleFix":"// before\nif (!res.ok) {\n  let msg = `Failed to stop task (${res.status})`;\n  ...\n// after — mirror the trigger path and humanize 409\nif (!res.ok) {\n  let msg = `Failed to stop task (${res.status})`;\n  try { const data = await res.json(); if (data && data.detail) msg = data.detail; } catch (_) {}\n  if (res.status === 409) msg = 'Task is not running';","handlingStrategy":"try-catch","validationCode":"const task = _tasks.find(t => t.id === id);\nif (!task || task.status !== 'active') { showToast('Task is not running'); return; }","typeGuard":null,"tryCatchPattern":"try { await _stopTask(id); } catch (e) { if (/\\(409\\)/.test(e.message) || /not running/i.test(e.message)) return; uiModule?.showError(e.message); }","preventionTips":["Only enable the Stop control when the task status is 'active'","Re-fetch status after run completion events","Ignore 'not running' races instead of erroring"],"tags":["http","frontend","tasks","api-client"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}