Skip to content

Return pending updates as UpdateStillPending and read outcomes later by id - #243

Merged
rmcdaniel merged 3 commits into
mainfrom
fix/212-update-outcome
Nov 13, 2026
Merged

rmcdaniel merged 3 commits into
mainfrom
fix/212-update-outcome

Conversation

@rmcdaniel

Copy link
Copy Markdown
Member

Part of durable-workflow/.github#212 (Rust slice). Uses the read route from server#1131 / server#1155 (native 3.6.0), with the same semantics as sdk-php 3.15.0's updateOutcome().

Bug: update_workflow waits for completion. When the Server's wait ran out first (HTTP 202, wait_timed_out: true, result: null), it returned Ok(Null). That looks exactly like a handler that returned null.

Change

  • A pending completion-wait receipt now returns Error::UpdateStillPending(UpdatePending) with update_id, request_id, current run_id, update_status, wait_timed_out and the full receipt. The message says how to read it again by update id, or how to resubmit with the same request_id on an older Server. A receipt with a result_envelope is finished, so an encoded null is still Ok(Null). update_workflow_avro_value returns the same error where it used to return a missing_payload_envelope codec error.
  • New Client::update_outcome / update_outcome_avro_value (wait for completion, decode the result) and update_outcome_receipt(…, UpdateWaitStage, …) (raw receipt), plus WorkflowHandle::update_outcome[_avro_value]. Each is a GET /workflows/{id}/updates/{update_id} with optional wait_for=completed and wait_timeout_seconds. The transport timeout is extended to cover the wait.
  • Capability gating: cluster info must advertise capabilities.update_outcome_reads: true, and the client caches it once seen. Otherwise these calls return Error::UpdateOutcomeReadsUnavailable after discovery, without reading anything, and the update is never resubmitted.
  • A failed update stays Error::Http 422 with the failure receipt, the same as update_workflow. A rejected update keeps its original status. New Error::is_update_not_found() detects 404 update_not_found.
  • Added the metrics route template /workflows/{workflow_id}/updates/{update_id}. README section with an example (compiled as a doctest), plus a CHANGELOG entry under Unreleased.

Behaviour change: callers that got Ok(Null) for a timed-out update now get Error::UpdateStillPending. The CHANGELOG notes this.

Tests (src/update_outcome/tests.rs, mocked HTTP): completed value and completed null; typed Avro read; pending read, which raises a typed error with a hint, followed by a re-read that returns the result, all over GET; accepted receipt read; failed 422; 404 update_not_found; an older Server without the capability, which gets only discovery and no read; an empty update id; a submitted update that times out, which is now pending rather than null for both untyped and typed calls; a submitted update that returned null.

Local (rust 1.86.0 in Docker): cargo fmt --all --check, cargo test --locked --all-targets --all-features (986 passed, 0 failed), doctests, and RUSTDOCFLAGS=-D warnings cargo doc --all-features --no-deps all clean.

Not covered here: an integration test against a published Server image. #212's acceptance asks for one, and it needs a published native 3.6.0+ image in the qualification jobs.

🤖 Generated with Claude Code

…by id (#212)

update_workflow returned the Server's result: null for a 202
wait_timed_out receipt, so an update still running looked like a handler
that returned null. A pending receipt now returns
Error::UpdateStillPending with the update id, request id, current run and
full receipt; a real null result is still Ok(Null).

Client::update_outcome, update_outcome_avro_value and
update_outcome_receipt (and WorkflowHandle::update_outcome) call
GET /workflows/{id}/updates/{update_id} (server#1131) on a Server that
advertises update_outcome_reads. Without the capability they return
Error::UpdateOutcomeReadsUnavailable before reading anything and never
resubmit the update. Error::is_update_not_found detects 404
update_not_found.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rmcdaniel

Copy link
Copy Markdown
Member Author

Security: CHANGES REQUESTED at 83252cd. A small fix: a confused read can come back as success.

update_outcome percent-encodes both ids with percent_encode_path_segment, which encodes / ? # but leaves . alone. reqwest then collapses dot segments: an update id .. becomes GET /api/workflows/{wf}/, and a workflow id .. becomes GET /api/updates/{id}. Any 200 body without update_status or wait_timed_out counts as finished (update_outcome.rs:140-150, 215). So if the server answers the collapsed path, for example with the workflow describe route, the caller gets Ok(<describe.result>) or Ok(Null) for an update that was never read. It stays within the same namespace and auth, and nothing is written, but it's a false success.

Requested:

  1. Refuse empty, . and .. ids before sending. RawRequest already has this check (raw_request.rs:140), so reuse it.
  2. Return a result only when the receipt has update_status == "completed" or an envelope, and its update_id (and workflow_id when present) matches the request, like cli#144 does.
  3. Add tests for / ? # . .. ids, for cluster info that fails or is malformed (refuse, no read), and for a plain route-missing 404 versus a typed update_not_found.

Everything else passes:

  • GET only, with an empty body, and nothing resubmits the update.
  • The same base URL, X-Namespace, version header and bearer token as the shared request path.
  • The capability gate runs before the read, fails closed, and caches only a positive answer.
  • A plain 404 is never treated as a typed not-found.
  • The null-versus-pending correctness fix is confirmed and tested both ways.

Separate, outside this PR: update_workflow_response and describe_workflow interpolate workflow_id into the path raw (lib.rs:3780, 3790). Please follow up there with the same segment encoding.

…ds (#212)

Security review of #243: an update id of "." or ".." collapsed the read
onto the workflow describe route, and any 200 body without update_status
was treated as finished, so update_outcome could return the workflow's
result or Null for an update it never read.

Workflow and update ids now go through RawRequest's segment check and an
empty, "." or ".." id is refused before any request. A successful answer
must be a receipt for the requested update (matching update_id, an
update_status, and the workflow id when echoed), and update_outcome only
returns a value from a completed receipt; anything else is
Error::UnrecognizedUpdateOutcome.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rmcdaniel

Copy link
Copy Markdown
Member Author

Security PASS at c4c96e1, on green CI. This resolves my change request. Both ids now go through RawRequest's segment check, so empty, . and .. are refused before any request, including before the capability check. is_receipt_for requires the requested update_id, a string update_status, and a matching workflow_id when one is echoed. Anything else is Error::UnrecognizedUpdateOutcome, whose display text doesn't print the receipt. The tests cover a describe-like body, an empty body, and a wrong update or workflow. A confused read can no longer come back as Ok.

@sonarqubecloud

Copy link
Copy Markdown

@rmcdaniel
rmcdaniel merged commit cead300 into main Nov 13, 2026
19 checks passed
@rmcdaniel rmcdaniel mentioned this pull request Nov 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant