odysseus-dev/odysseus · error · Error

Failed to trigger task (${res.status})

Error message

Failed to trigger task (${res.status})

What it means

Thrown by the tasks UI when POST /api/tasks/{id}/run (trigger) returns a non-2xx status. The client first tries to parse the backend's JSON error detail and use it as the message; this literal message is the fallback when the response body has no parseable `detail` field. A 409 is special-cased to 'Task is already running' regardless of body.

Source

Thrown at static/js/tasks.js:163

  });
  if (!res.ok) throw new Error('Failed to resume task');
}

async function _runNow(id, force = false) {
  const res = await fetch(`${API_BASE}/api/tasks/${id}/run${force ? '?force=true' : ''}`, {
    method: 'POST', credentials: 'same-origin',
  });
  if (!res.ok) {
    // Surface the backend's actual reason — 409 means "already running",
    // 404 task missing, etc. Previously every error rendered as the same
    // generic "Failed to trigger task", which hid the cause.
    let msg = `Failed to trigger task (${res.status})`;
    try {
      const data = await res.json();
      if (data && data.detail) msg = data.detail;
    } catch (_) {}
    if (res.status === 409) msg = 'Task is already running';
    throw new Error(msg);
  }
}

async function _stopTask(id) {
  const res = await fetch(`${API_BASE}/api/tasks/${id}/stop`, {
    method: 'POST',
    credentials: 'same-origin',
  });
  if (!res.ok) {
    let msg = `Failed to stop task (${res.status})`;
    try {
      const data = await res.json();
      if (data && data.detail) msg = data.detail;
    } catch (_) {}
    throw new Error(msg);
  }
}

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Check res.status in the thrown error or devtools network tab: 404 = stale task id (refresh the task list), 401/403 = re-login, 500 = inspect server logs
  2. Verify the task id still exists by re-fetching GET /api/tasks before triggering
  3. Confirm the page origin matches API_BASE (window.location.origin) so the same-origin cookie is sent
  4. If behind a proxy, ensure /api/tasks/* is routed to the app and error responses stay JSON

Example fix

// before
throw new Error(msg); // msg falls back to 'Failed to trigger task (status)'
// after — include endpoint context so the failing call is identifiable
throw new Error(msg === `Failed to trigger task (${res.status})` ? `Failed to trigger task ${id} (${res.status} ${res.statusText})` : msg);
Defensive patterns

Strategy: try-catch

Validate before calling

// Verify task exists before triggering
const tasks = await (await fetch(`${API_BASE}/api/tasks`, { credentials: 'same-origin' })).json();
if (!tasks.tasks?.some(t => t.id === id)) { uiModule?.showError('Task no longer exists'); return; }

Try / catch

try { await _triggerTask(id); } catch (e) { if (/\(409\)|already running/.test(e.message)) { /* benign */ } else if (/\(40[13]\)/.test(e.message)) { await refreshLoginOrTasks(); } else { uiModule?.showError(e.message); } }

Prevention

When it happens

Trigger: Clicking Run/Trigger on a task whose backend run endpoint fails: task id no longer exists (404), auth/session cookie expired (401/403), backend scheduler error (500), or a non-JSON error body (e.g. HTML from a proxy) so data.detail is missing.

Common situations: Stale task list in the browser after tasks were redefined on the server; reverse proxy intercepting the API route; session cookie expired because credentials are 'same-origin' but the page was loaded from a different origin.

Related errors


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