{"record":{"id":"be391a14a7aafa30","repo":"can1357/oh-my-pi","slug":"retry-requires-delivery-id-or-issue","errorCode":null,"errorMessage":"retry requires 'delivery_id' or 'issue'","messagePattern":"retry requires 'delivery_id' or 'issue'","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"warning","filePath":"python/robomp/src/server.py","lineNumber":655,"sourceCode":"                status_code=202,\n            )\n\n        # mode == \"retry\"\n        if isinstance(delivery_id, str) and delivery_id:\n            target = delivery_id\n        elif isinstance(issue_ref, str) and issue_ref:\n            try:\n                repo_full, number = parse_issue_ref(issue_ref)\n            except InvalidIssueRef as exc:\n                raise HTTPException(400, str(exc)) from exc\n            if not cfg.allows(repo_full):\n                raise HTTPException(403, f\"{repo_full} not in ROBOMP_REPO_ALLOWLIST\")\n            row = db.latest_event_for_issue(make_issue_key(repo_full, number))\n            if row is None:\n                raise HTTPException(404, f\"no retryable stored event for {repo_full}#{number}\")\n            target = row.delivery_id\n        else:\n            raise HTTPException(400, \"retry requires 'delivery_id' or 'issue'\")\n\n        event = db.get_event(target)\n        if event is None:\n            raise HTTPException(404, f\"unknown delivery {target}\")\n        if not db.requeue_event(target, from_states=INACTIVE_EVENT_STATES):\n            raise HTTPException(409, f\"delivery {target} is {event.state}; only inactive events can be retried\")\n        pool.wake()\n        log.info(\"manual retry\", extra={\"delivery\": target})\n        return JSONResponse(\n            {\"delivery\": target, \"state\": \"queued\", \"mode\": \"retry\"},\n            status_code=202,\n        )\n\n    @app.post(\"/api/cancel\")\n    async def api_cancel(\n        request: Request,\n        payload: dict[str, Any] = Body(...),\n        x_robomp_token: str | None = Header(None, alias=\"X-Robomp-Replay-Token\"),","sourceCodeStart":637,"sourceCodeEnd":673,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/python/robomp/src/server.py#L637-L673","documentation":"The retry endpoint requires an identifier to act on and raises HTTP 400 'retry requires delivery_id or issue' when the request body contains neither. The endpoint accepts either a stored delivery_id or an issue reference (`owner/repo#N`) to locate the event; an empty or non-string value for both leaves it with no target, so the request is rejected outright.","triggerScenarios":"POSTing to the retry endpoint with a body missing both 'delivery_id' and 'issue' — e.g. `{}`, `{'delivery_id': ''}` (empty string), `{'delivery_id': 123}` (not a string), or `{'issue': None}`. The guard is `isinstance(delivery_id, str) and delivery_id` / `isinstance(issue_ref, str) and issue_ref`.","commonSituations":"Forgetting the JSON body entirely or sending it with the wrong Content-Type so the body parses to empty; a client serializing an undefined/null variable; integer delivery IDs being sent where a string is required; renaming the parameter in a client without updating it server-side.","solutions":["Include a non-empty string 'delivery_id' in the JSON body: {\"delivery_id\": \"<id>\"}","Alternatively include a well-formed issue reference: {\"issue\": \"owner/repo#123\"}","Ensure the request has Content-Type: application/json and the body is actually sent","Convert numeric/UUID delivery values to strings before sending"],"exampleFix":"// before\nawait fetch('/retry', { method: 'POST', body: JSON.stringify({}) });\n// after\nawait fetch('/retry', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ delivery_id: String(deliveryId) }) });","handlingStrategy":"validation","validationCode":"// Ensure exactly one valid target field before calling retry\nconst body = deliveryId\n  ? { delivery_id: String(deliveryId) }\n  : { issue: `${owner}/${repo}#${number}` };\nif (!body.delivery_id && !body.issue) throw new Error('retry needs delivery_id or issue');","typeGuard":"function hasRetryTarget(b: unknown): b is { delivery_id?: string; issue?: string } {\n  const o = b as Record<string, unknown>;\n  return (typeof o?.delivery_id === 'string' && o.delivery_id.length > 0) ||\n         (typeof o?.issue === 'string' && o.issue.length > 0);\n}","tryCatchPattern":"try {\n  await api.retry(body);\n} catch (err) {\n  if (err.status === 400 && /retry requires/.test(err.message)) {\n    logger.error('retry call missing target', { body });\n  } else throw err;\n}","preventionTips":["Always send the body with Content-Type: application/json","Stringify delivery ids — the server only accepts string values","Keep one shared client function for retry so the target field is never forgotten","Validate the body in CI tests against the endpoint contract"],"tags":["http-400","bad-request","missing-parameter","validation","api"],"backgroundTag":"missing-required-parameter","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}