{"record":{"id":"d84bad10f2c64b89","repo":"can1357/oh-my-pi","slug":"delivery-delivery-id-is-event-state-only-runn","errorCode":null,"errorMessage":"delivery {delivery_id} is {event.state}; only running deliveries can be cancelled","messagePattern":"delivery (.+?) is (.+?); only running deliveries can be cancelled","errorType":"http","errorClass":"HTTPException","httpStatus":409,"severity":"info","filePath":"python/robomp/src/server.py","lineNumber":691,"sourceCode":"        x_robomp_token: str | None = Header(None, alias=\"X-Robomp-Replay-Token\"),\n    ) -> JSONResponse:\n        \"\"\"Stop a running event. The omp subprocess is killed; the row lands in\n        `failed` with `cancelled by operator` as the error.\n        \"\"\"\n        bag = request.app.state.bag\n        cfg: Settings = bag[\"settings\"]\n        _require_trigger_token(cfg, x_robomp_token)\n\n        delivery_id = payload.get(\"delivery_id\")\n        if not isinstance(delivery_id, str) or not delivery_id:\n            raise HTTPException(400, \"cancel requires 'delivery_id'\")\n\n        db: Database = bag[\"db\"]\n        event = db.get_event(delivery_id)\n        if event is None:\n            raise HTTPException(404, f\"unknown delivery {delivery_id}\")\n        if event.state != \"running\":\n            raise HTTPException(\n                409, f\"delivery {delivery_id} is {event.state}; only running deliveries can be cancelled\"\n            )\n\n        pool: WorkerPool = bag[\"pool\"]\n        fired = await pool.cancel_event(delivery_id)\n        log.info(\n            \"manual cancel\",\n            extra={\"delivery\": delivery_id, \"fired\": fired, \"state\": event.state},\n        )\n        return JSONResponse(\n            {\"delivery\": delivery_id, \"fired\": fired, \"previous_state\": event.state},\n            status_code=202,\n        )\n\n    @app.get(\"/events\")\n    async def events(request: Request, limit: int = 50) -> dict[str, Any]:\n        rows = request.app.state.bag[\"db\"].list_events(limit=limit)\n        return {","sourceCodeStart":673,"sourceCodeEnd":709,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/python/robomp/src/server.py#L673-L709","documentation":"FastAPI raises HTTP 409 with 'delivery {id} is {state}; only running deliveries can be cancelled' from POST /api/cancel when the event exists but its recorded state is not 'running'. Cancelling only applies to in-flight tasks; completed, failed, or queued deliveries cannot be cancelled, only retried (inactive states) via /api/trigger retry mode.","triggerScenarios":"POST /api/cancel for a delivery whose event.state is 'done' (already finished), 'failed' (already errored or previously cancelled), or 'queued' (not yet claimed by the dispatcher); double-clicking cancel after the first cancel already flipped the row to failed.","commonSituations":"Operator sees a stale dashboard listing and cancels an already-finished task; two maintainers cancel concurrently; racing the worker which completed the task between fetch and cancel; trying to cancel a queued event that should be retried/cancelled via trigger instead.","solutions":["Check GET /events for the delivery's current state; if it is done/failed, nothing needs cancelling.","For queued deliveries use POST /api/trigger with mode 'retry' (requeues inactive events) rather than cancel.","Refresh the dashboard and retry only if the event is genuinely still running; the state row may have been stale.","Treat the 409 as informational — the task already reached a terminal state; inspect last_error in /events if it failed."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"const { events } = await (await fetch('/events')).json();\nconst ev = events.find(e => e.delivery_id === deliveryId);\nif (!ev) throw new Error('unknown delivery');\nif (ev.state !== 'running') throw new Error(`delivery is ${ev.state}; only running deliveries can be cancelled`);","typeGuard":null,"tryCatchPattern":"try {\n  await api.cancel(deliveryId);\n} catch (e) {\n  if (e instanceof ApiError && e.status === 409) {\n    console.info('already finished — nothing to cancel; use trigger/retry if needed');\n  } else throw e;\n}","preventionTips":["Check the delivery state via GET /events immediately before cancelling.","Refresh the dashboard view; stale listings cause double-cancels.","Use POST /api/trigger (retry mode) for inactive deliveries instead of cancel.","Handle double-click/multiple operators with optimistic UI disable after the first cancel."],"tags":["http-409","conflict","state-machine","fastapi"],"backgroundTag":"invalid-state-transition","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}