Skip to content

POST /api/v1/approvals/requests/{id}/reject returns 500 while its effect lands AND strands the workflow run — three inconsistent outcomes from one call #13807

Description

@os-warren

Split out of #13568 by the triage seat. #13568 was ruled by the maintainer at 2026-08-31T02:20Z (director batch #5, 「同意」). The evidence below was added to that card at 02:54Z and 03:57Z — after the ruling — and describes a defect the ruling does not reach: #13568's remedy (auto-cancel pending approvals when a record is deleted) prevents future orphans; it says nothing about a write path that returns an error while applying its effect, and nothing about the runs already stranded.

The measurement (from #13568's follow-up comments, reporter yinlianghui, re-read from transcript by the filing session — not recalled)

One call against an orphaned sys_approval_request (its record_id points at a deleted crm_leave_request row):

POST /api/v1/approvals/requests/{approvalRequestId}/reject     body {"comment": "<text>"}
  → HTTP 500

and afterwards three states coexist:

  1. the caller received 500;
  2. the request's status is rejected, and it has disappeared from the pending inbox — the effect landed;
  3. the workflow run is stranded — unrecoverable.

The full error text (the first quote of it in #13568 was truncated; the reporter corrected it in the third comment, which is the version to trust):

…ow run 'run_8380f743-ac5b-4c27-a976-9ac5e71f6a00' could not be resumed and is
now stranded: resume of run 'run_8380f743-ac5b-4c27-a976-9ac5e71f6a00' failed:
Node 'mark_rejected' failed: update_record(crm_leave_request) failed:
Record 9SEmlyRfw8D9-J7Z not found in crm_leave_request

Controls from the same session: POST …/approve on a healthy request → 200. DELETE /api/v1/data/sys_approval_request/{id}405 (so the orphan cannot be cleaned up through the data API either).

Why this is p1 and separate from #13568

  • The 500 lies about the outcome. A caller — human, script, or agent — reads 500 as "the rejection did not happen" and retries or escalates. It did happen. This is the error/effect divergence class, and it is worse than a plain failure because the honest-looking response is the wrong one.
  • stranded is unrecoverable and has no operator story on this card. [finding] 删除记录后,它的审批单仍留在审批收件箱:pending 与 approved 两种都留,记录引用退化为原始 id #13568's ruling produces no mechanism for runs already in that state, because it did not know about them.
  • The trigger generalises past approvals. mark_rejected fails because a workflow node writes back to a record that no longer exists. Any workflow node doing update_record on a since-deleted row plausibly reaches the same strand. ⚠️ Not measured — that generalisation is a hypothesis, and establishing its reach is this card's first step, not its premise.

First deliverables, in order

  1. Establish the reach. Does any workflow node failing mid-resume strand the run, or is this specific to the approval resume path? That decides whether the fix is in plugin-approvals or in the workflow engine, and it may re-anchor this card's lane.
  2. Decide the atomicity posture for the reject door: either the status write and the workflow resume succeed together, or neither does. ⛔ Returning 500 after a committed status write is not an option to preserve.
  3. An operator path out of stranded — even if it is only a documented manual step. A state a deployment can enter and never leave is not acceptable as an end state, and the 405 on the data API means there is currently no way out at all.

⛔ Boundaries

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions