Problem
POST /tasks/{id}/complete fails with 404 when the task has already been completed, so a retried completion cannot be distinguished from a genuinely unknown task id.
Path today:
TaskCompleteProcessor::process() forwards to FlowableClient::completeTask()
- the engine answers
404 for a task that is no longer in ACT_RU_TASK
FlowableApiException::fromUpstreamStatus() maps upstream 404 → HTTP 404 Flowable resource not found.
Why that hurts
Task completion is a natural at-least-once operation: the client may lose the response (network drop, PHP-FPM timeout, a mobile client syncing queued work later) and retry. The retry is semantically a no-op — the task is completed — but the caller sees a hard 404 and has no way to tell "already done" from "bad task id".
It gets worse for consumers that decorate the processor to write their own domain record before forwarding the completion (write-first / complete-second, so a failed engine call leaves the task open for an idempotent retry). There, the happy-path retry — domain record already written, engine already completed — is the one that returns 404, which reads as a failure to every layer above.
Proposed fix
On upstream 404 from completeTask(), check whether the task exists in history and has ended:
// TaskCompleteProcessor
try {
$client->completeTask($taskId, $payload);
} catch (FlowableApiException $e) {
if ($e->getStatusCode() !== 404 || !$this->wasCompleted($client, $taskId)) {
throw $e;
}
// already completed → the request's intent is satisfied
}
with wasCompleted() = findHistoricTask($taskId) and a non-null endTime. FlowableClientInterface::findHistoricTask() already exists, and the extra roundtrip only happens on the error path, so the happy path is unchanged.
A task id that is unknown to history keeps returning 404. Worth deciding: whether the idempotent success should be distinguishable from a fresh completion (e.g. a flag in the audit entry — $this->audit('task.complete', ['task' => $taskId, 'alreadyCompleted' => true])), and whether this should be default behaviour or opt-in via bundle config for consumers that rely on the current 404.
Suggested test
Two cases against a mocked client: (1) completeTask() throws upstream 404 + findHistoricTask() returns ['endTime' => '…'] ⇒ no exception; (2) same 404 + findHistoricTask() returns null ⇒ FlowableApiException (404) propagates.
Problem
POST /tasks/{id}/completefails with 404 when the task has already been completed, so a retried completion cannot be distinguished from a genuinely unknown task id.Path today:
TaskCompleteProcessor::process()forwards toFlowableClient::completeTask()404for a task that is no longer inACT_RU_TASKFlowableApiException::fromUpstreamStatus()maps upstream404→ HTTP404 Flowable resource not found.Why that hurts
Task completion is a natural at-least-once operation: the client may lose the response (network drop, PHP-FPM timeout, a mobile client syncing queued work later) and retry. The retry is semantically a no-op — the task is completed — but the caller sees a hard
404and has no way to tell "already done" from "bad task id".It gets worse for consumers that decorate the processor to write their own domain record before forwarding the completion (write-first / complete-second, so a failed engine call leaves the task open for an idempotent retry). There, the happy-path retry — domain record already written, engine already completed — is the one that returns
404, which reads as a failure to every layer above.Proposed fix
On upstream
404fromcompleteTask(), check whether the task exists in history and has ended:with
wasCompleted()=findHistoricTask($taskId)and a non-nullendTime.FlowableClientInterface::findHistoricTask()already exists, and the extra roundtrip only happens on the error path, so the happy path is unchanged.A task id that is unknown to history keeps returning
404. Worth deciding: whether the idempotent success should be distinguishable from a fresh completion (e.g. a flag in the audit entry —$this->audit('task.complete', ['task' => $taskId, 'alreadyCompleted' => true])), and whether this should be default behaviour or opt-in via bundle config for consumers that rely on the current 404.Suggested test
Two cases against a mocked client: (1)
completeTask()throws upstream 404 +findHistoricTask()returns['endTime' => '…']⇒ no exception; (2) same 404 +findHistoricTask()returnsnull⇒FlowableApiException(404) propagates.