{"record":{"id":"a97d651b05fa3fb3","repo":"different-ai/openwork","slug":"approval-not-found","errorCode":"approval_not_found","errorMessage":"Approval request not found","messagePattern":"Approval request not found","errorType":"http","errorClass":"ApiError","httpStatus":404,"severity":"warning","filePath":"apps/server/src/routes/operations.ts","lineNumber":67,"sourceCode":"      action: \"engine.reload\",\n      target: workspace.baseUrl ?? \"opencode\",\n      summary: \"Reloaded workspace engine\",\n      timestamp: Date.now(),\n    });\n\n    return jsonResponse({ ok: true, reloadedAt: Date.now() });\n  });\n\n  addRoute(routes, \"GET\", \"/approvals\", \"host\", async (ctx) => {\n    return jsonResponse({ items: ctx.approvals.list() });\n  });\n\n  addRoute(routes, \"POST\", \"/approvals/:id\", \"host\", async (ctx) => {\n    const body = await readJsonBody(ctx.request);\n    const reply = body.reply === \"allow\" ? \"allow\" : \"deny\";\n    const result = ctx.approvals.respond(ctx.params.id, reply);\n    if (!result) {\n      throw new ApiError(404, \"approval_not_found\", \"Approval request not found\");\n    }\n    return jsonResponse({ ok: true, allowed: result.allowed });\n  });\n}\n","sourceCodeStart":49,"sourceCodeEnd":72,"githubUrl":"https://github.com/different-ai/openwork/blob/2b7df46e8ae1517d64c896c7793d2d52ec845669/apps/server/src/routes/operations.ts#L49-L72","documentation":"The POST /approvals/:id route resolves a pending approval request by id via ctx.approvals.respond and denies it by default unless body.reply === \"allow\". If no approval with that id exists (already responded, expired, wrong id, or routed to the wrong server), respond returns null and the route throws ApiError 404 code \"approval_not_found\".","triggerScenarios":"Answering an approval twice (the first response consumes it); the approval timed out or the server restarted losing in-memory state; a typo'd/mismatched approval id; replying on a different host connection than the one that created the request.","commonSituations":"Double-click on an Approve/Deny button sends two POSTs; a long-running request expired before the user answered; the client cached an old approval list; multiple server instances with per-process approval stores.","solutions":["Treat 404 as 'already handled': refresh the pending approvals list and proceed — the operation was allowed or denied by the earlier response.","Guard the client against duplicate submissions (disable the button after the first POST, dedupe by approval id).","Verify you are posting to the same server/session that issued the approval and that the id comes from the current pending set (e.g. via the approvals list/event stream)."],"exampleFix":"// before\nawait Promise.all([respond(id, \"allow\"), logApproval(id)]); // second POST may 404\n// after\ntry {\n  await respond(id, \"allow\");\n} catch (e) {\n  if (e.code === \"approval_not_found\") await refreshApprovals(); // already resolved\n  else throw e;\n}","handlingStrategy":"try-catch","validationCode":"const pending = await api.listApprovals();\nif (!pending.some((a) => a.id === approvalId)) return; // already resolved or expired — skip POST","typeGuard":"function isPendingApproval(a) { return typeof a === \"object\" && a !== null && typeof a.id === \"string\" && a.status === \"pending\"; }","tryCatchPattern":"try {\n  await api.respondToApproval(approvalId, \"allow\");\n} catch (e) {\n  if (e.status === 404 && e.code === \"approval_not_found\") {\n    await refreshApprovals(); // idempotent: someone already answered\n  } else throw e;\n}","preventionTips":["Make approval buttons single-submit (disable after click, dedupe by id).","Subscribe to approval events and drop resolved ids from the pending list.","Handle 404 as a benign 'already handled' outcome, not a crash."],"tags":["http-404","approvals","race-condition"],"backgroundTag":"resource-not-found-404","analyzedSha":"2b7df46e8ae1517d64c896c7793d2d52ec845669","analyzedAt":"2026-09-01T07:59:23.713Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}