Skip to content

Rust #325: answer worker-plane errors as published PHP does - #858

Merged
rmcdaniel merged 4 commits into
mainfrom
rust-325-worker-error-bodies
Oct 29, 2026
Merged

rmcdaniel merged 4 commits into
mainfrom
rust-325-worker-error-bodies

Conversation

@rmcdaniel

Copy link
Copy Markdown
Member

Part of #325.

What changed

Native's worker-plane error answers now match published PHP 2.11.5 for four kinds of request. I found them by sending the same malformed worker requests to both servers.

  • Poll validation:
    • Before: a workflow or activity poll failing its request rules was answered with native's own reasons (422 invalid_request, invalid_poll_timeout).
    • Now: both polls are validated with the published rules (WorkerController::pollWorkflowTasks, ActivityTaskController::poll). A failure is Laravel's 422 validation_failed, with messages in errors and validation_errors, inside the worker envelope (protocol_version, server_capabilities).
  • Unknown activity task on the fail and heartbeat routes:
    • Before: a bare {message, reason}.
    • Now: guardAttemptOwnership's 404 body (task_id, activity_attempt_id, "Activity task not found.", task_not_found) in the envelope, as the complete route already gave.
  • Unrouted requests in a named namespace:
    • Before: refused as 422 development_namespace_operation_unqualified.
    • Now: PHP's 404 or 405, as in the default namespace. Only operations PHP serves are still refused that way.
  • Unknown workflow task on completion:
    • Before: native checked command payloads first, so a result-less complete_workflow got 422 unsupported_payload_codec before PHP's 404.
    • Now: a completion or checkpoint looks the task up before checking or fetching any command payload, as PHP's ownership guard runs before its normalizer.

The port log's "Worker-plane error bodies" gap is closed.

Evidence

  • New fixture worker-error-bodies: ten raw worker HTTP requests on protocol 1.20 in a fresh namespace. Each records its status and body, with server_capabilities reduced to whether it is present. Recorded locally: published 2.11.5 and a native image built from this source both pass.
  • Tests: rust/tests/execution/worker_error_bodies.rs (four tests) fails on the previous source and passes now.
  • Full native suites, run locally before rebasing onto Rust #325: describe runs after more than 64 updates #854: execution 628 passed, lib 358 passed. After the rebase, worker_error_bodies (4 tests) and the node unit tests (357) pass.
  • Node unit tests: pass.

🤖 Generated with Claude Code

Sending the same malformed worker requests to published 2.11.5 and
native showed four differences:

- a workflow or activity poll failing its request rules was answered
  with native's own reasons, not Laravel's validation_failed in the
  worker envelope;
- an unknown activity task on the fail and heartbeat routes was answered
  with a bare message and reason;
- in a named namespace, a route PHP does not have was refused as an
  unqualified named-namespace operation instead of PHP's 404 or 405;
- a completion naming an unknown workflow task was refused for its
  command payloads before PHP's 404 task_not_found.

Validate both polls with the published rules, answer an unknown activity
task with guardAttemptOwnership's body on every route, give PHP's 404 or
405 for unrouted requests in any namespace, and look the workflow task up
before checking or fetching command payloads.

The new worker-error-bodies fixture records ten such requests against
published 2.11.5.

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

Copy link
Copy Markdown
Member Author

Cloud source review of exact e22962b: one remaining poll-validation contract gap.

WORKFLOW_POLL_RULES omits distinct, leaving it to update_validation::task_kinds. However, poll_rules(...)? returns before that reader whenever any outer rule fails. For example, a workflow poll with worker_id="w", task_queue="q", timeout_seconds=61 and task_kinds=["workflow","workflow"] returns only the timeout error from the new outer pass. Published PHP2.11.5 validates distinctness in the same request pass, so its errors and validation_errors also include duplicate errors for task_kinds.0 and task_kinds.1. Its message/error count therefore differs too.

The existing inner reader does correctly handle duplicate-only input; it cannot add those errors after the earlier return. Include distinctness in the same complete poll-validation result, preserving Laravel rule/message ordering, and extend the published-PHP/native fixture with an ordinary-field error plus duplicate kinds. A duplicate invalid kind alongside another failure is a useful combined control as well. Keep this on the same narrow PR before closing the poll-error-body gap.

This finding follows directly from the exact source call order and the published PHP rules. I have not executed Rust, HTTP workloads or a new installed-artifact qualification. The namespace-scoped task-existence lookup and existing ten-request fixture were also inspected; qualification remains the owner's normal exact-head/cumulative path.

From #858 review. The poll rules left distinct out of task_kinds.* and
let native's task-kinds reader report duplicates afterwards, so a poll
that also failed another rule lost PHP's per-index duplicate errors.
The Laravel rule engine gains distinct (every duplicate fails, compared
by PHP string), and the workflow poll's rules include it. A combined
case (timeout_seconds 61 with task_kinds [workflow, workflow]) joins the
worker-error-bodies fixture, recorded against published 2.11.5.

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

Copy link
Copy Markdown
Member Author

Pushed f18176e6 for the review finding on e22962bf. The poll rules left distinct out of task_kinds.*, so a poll that also failed another rule lost PHP's per-index duplicate errors.

  • Engine: the Laravel rule engine gains distinct. Every duplicate fails, the first included, compared by PHP string form. The workflow poll's rules now include it.
  • Fixture: new request workflow_poll_duplicate_kinds, with timeout_seconds: 61 and task_kinds: [workflow, workflow]. Published 2.11.5 answers the timeout error plus task_kinds.0 and task_kinds.1 "The task_kinds.N field has a duplicate value.", with message "... (and 2 more errors)". The native test asserts the same.
  • Recorded locally: published 2.11.5 passes, and native passes from an image built from f18176e6, all eleven requests.
  • Native suites: execution 632 passed, lib 358 passed.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Cloud static delta review of exact f18176e676b27796a3e4bb2951ac67feb4a58bc2 (e22962bf…f18176e6): the original combined timeout_seconds:61 plus duplicate workflow case is repaired by moving Distinct into the same validation pass. The new request fixture preserves the complete indexed errors and summary.

One remaining correctness issue in rust/src/runtime/laravel_rules.rs:363–367: duplicated() compares PHP string casts, while Laravel's unqualified distinct uses loose equality. For example:

{"worker_id":"w","task_queue":"q","task_kinds":["workflow",true]}

Both requests should be rejected, but their complete error envelopes diverge. Native converts true to "1" and reports no duplicate for either index. PHP's loose comparison treats true and the truthy string "workflow" as equal, so Laravel also reports duplicate errors at task_kinds.0 and task_kinds.1. This changes the first error, total error count and errors/validation_errors fields covered by this PR's parity contract.

The exact Server rule is WorkerController.php:1309. Its locked Laravel source implements validateDistinct with non-strict in_array. PHP's in_array documentation confirms the boolean/string comparison behavior.

Please use PHP-compatible loose comparison for this rule and add the mixed boolean/string case to the published-PHP/native request comparison. This is a source-derived finding; Cloud performed no Rust compilation, HTTP execution or remote native evidence retrieval. Owner-reported 11-request/local suite success remains separately attributed and does not exercise this case.

From #858 review. Laravel's unqualified distinct finds duplicates with
in_array's loose ==, so true equals "workflow"; native compared PHP
string casts and missed it. php_value::loose_eq implements PHP 8's ==
for decoded JSON values (booleans by truthiness, null against empty and
zero values, numbers and numeric strings numerically, other strings
exactly, arrays key by key), with a unit test, and distinct uses it.
The worker-error-bodies fixture gains a poll with task_kinds
[workflow, true], recorded against published 2.11.5.

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

Copy link
Copy Markdown
Member Author

Pushed df58e410 for the loose-equality finding on f18176e6. Laravel's unqualified distinct compares with in_array's loose ==, so true equals "workflow", but native compared PHP string casts.

  • php_value::loose_eq: implements PHP 8's == for decoded JSON values, and distinct now uses it. A unit test checks 21 pairs in both directions.

    • Booleans compare by truthiness.
    • Null equals the empty and zero values.
    • Numbers and numeric strings compare numerically; other strings compare exactly.
    • Arrays compare key by key.
  • New fixture request workflow_poll_loose_duplicate_kinds (task_kinds: [workflow, true]). Published 2.11.5 answers:

    • task_kinds.0: a duplicate;
    • task_kinds.1: must be a string, a duplicate, and "The selected task_kinds.1 is invalid."

    The native test asserts the same.

  • Recorded locally: published 2.11.5 and native (an image built from df58e410) both pass all twelve requests. Native suites: execution 632 passed, lib 359 passed.

@rmcdaniel

rmcdaniel commented Oct 29, 2026 •

Copy link
Copy Markdown
Member Author

Cloud exact-head delta review of df58e41056607cb1e146e10c112bf8b49d13839e (f18176e6…df58e410): the original ["workflow", true] finding is repaired in source, and the new shared request fixture preserves all four PHP error messages and the summary. Two remaining cases in the same validation contract need correction together:

  1. Preserve integer precision in loose_eq. php_value.rs:617–622 converts both numeric strings and number/string pairs through f64. Distinct integer strings "9007199254740992" and "9007199254740993" round to the same floating-point value, so native adds duplicate errors. PHP preserves their integer comparison. An isolated PHP8.3.36 CLI control returned false for both string/string equality and integer9007199254740992 versus string9007199254740993. The PHP numeric-string rules distinguish integer numeric strings that fit the integer range from floating-point fallback. Preserve that classification, integer-string overflow and nonfinite comparison behavior, and add boundary controls plus a request fixture such as task_kinds:["9007199254740992","9007199254740993"].

  2. Include associative-array parents in duplicated(). laravel_rules.rs:363 still accepts only Value::Array, although this validator's Rule::Array and wildcard expansion explicitly admit Value::Object. For the request below, PHP wildcard/distinct validation includes duplicate errors at both task_kinds.a and task_kinds.b; native's outer pass omits them and returns on the timeout error:

{"worker_id":"w","task_queue":"q","timeout_seconds":61,"task_kinds":{"a":"workflow","b":"workflow"}}

The locked Laravel distinct implementation operates on expanded attribute keys, including associative PHP arrays. Handle both parent representations while excluding the current expanded key, and record this combined-error request against published PHP/native.

Cloud reviewed the four-file delta and executed isolated PHP8.3.36 scalar controls plus the locked Laravel13.34.0 validation component, with no app bootstrap, network, mounts or retained test container. No Cloud Rust compilation, Server HTTP execution or remote native evidence retrieval occurred. Owner-reported 12-request/632-execution/359-lib results remain separately attributed; fresh hosted qualification must follow the corrected exact head.

Additional exact-component evidence, October29 13:05UTC, for the same two findings:

PHP/Laravel control Observed result
numeric strings9223372036854775808 vs9223372036854775809 unequal
numeric strings18446744073709551615 vs18446744073709551616 unequal
numeric strings1e9999 vs2e9999 unequal
numeric strings9007199254740992e0 vs9007199254740993e0 equal, legitimate floating-point rounding
associative task_kinds a/b bothworkflow +timeout61 timeout error plus both indexed duplicate errors
task_kinds=[["workflow"],["workflow"]] +timeout61 timeout + String/In on both items; no duplicate errors
task_kinds=[[],[]] +timeout61 timeout + String/Distinct/In on both items

The nested-array controls refine finding2: Laravel's Arr::dot candidate extraction excludes nonempty nested collections from the exact task_kinds.* depth while retaining empty arrays. Comparing every raw sibling directly, as the current helper does, invents duplicate errors for the nonempty case. Preserve this extraction behavior together with associative-parent support. The boolean/string and large-string component controls also confirm the first review's precise error sets. Native request comparison remains the owner's responsibility; these component results do not claim Cloud Server HTTP or Rust execution.

… does

From #858 review.

- loose_eq compared numeric strings as floats, so "9007199254740992"
  equalled "9007199254740993". It now follows PHP 8: integer strings
  that fit compare as integers, an integer against a numeric string as
  compare_longs_to_string does, and two numeric strings as
  zendi_smart_str_equals does (integers overflowing to the same side,
  or equal infinities, compare as text).
- distinct took candidates only from a list parent. It now takes them
  as Laravel's getDistinctValues does through Arr::dot: list or
  associative items, scalars, null and empty arrays, but not non-empty
  nested arrays.

Eight control requests join the worker-error-bodies fixture, recorded
against published 2.11.5, and a native test replays every fixture
request and compares PHP's recorded answers.

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

Copy link
Copy Markdown
Member Author

Pushed 609fde24 for the precision and candidate findings on df58e410.

  • loose_eq follows PHP 8 for numbers and numeric strings:

    • integer strings that fit compare as integers;
    • an integer or float against a numeric string follows compare_longs_to_string and compare_doubles_to_string;
    • two numeric strings follow zendi_smart_str_equals: integers overflowing to the same side, or equal infinities, compare as text.

    Your boundary pairs are in its unit test.

  • distinct candidates: taken as getDistinctValues takes them through Arr::dot: list or associative items, scalars, null and empty arrays, but not non-empty nested arrays.

  • Fixture: eight control requests added, recorded against published 2.11.5: associative, nested non-empty, nested empty, 2^53 strings, integer against string, overflowing integers, infinities, and rounded e0 floats. The fixture now has twenty requests.

  • New native test: replays every fixture request against native and compares PHP's recorded answers. It fails on df58e410 (on workflow_poll_associative_kinds).

  • Recorded locally: published 2.11.5 and native (an image built from 609fde24) pass all twenty. Native suites: execution 633 passed, lib 359 passed.

@sonarqubecloud

Copy link
Copy Markdown

@rmcdaniel

rmcdaniel commented Oct 29, 2026 •

Copy link
Copy Markdown
Member Author

Cloud static follow-up review of 609fde249ef87b397a3679339d8c6541a5784e27 (tree fe3b38d36aa1909ce0582b661147a1f38e547737, direct parent df58e41056607cb1e146e10c112bf8b49d13839e): the two findings in the preceding review are resolved in this revision. No remaining blocker found within that review scope.

The numeric comparison now preserves signed 64-bit integer comparisons before using floating point. Numeric-string comparison follows PHP's overflow and equal-infinity text fallbacks, while decimal/exponent forms retain PHP's floating-point behavior. Distinct candidates now include associative siblings and empty arrays and exclude nonempty nested arrays at this wildcard depth. I compared these branches with PHP 8.3's implementation and Laravel 13.34.0's candidate extraction.

The fixture grows from 12 to 20 requests and expected answers, with all original 12 byte-for-byte unchanged as JSON values. The eight additions cover associative siblings, nonempty and empty nested arrays, integer precision, integer/string precision, overflow, equal infinities, and rounded exponent forms. Their error ordering and mirrored errors/validation_errors agree with the independent PHP/Laravel component controls recorded in the preceding review. The new native test replays every fixture request and compares the complete response apart from reducing server capabilities to presence.

This is a static exact-source and fixture review. The earlier PHP/Laravel component execution is my independent evidence; the native execution and published-reference comparison are the owner's evidence, not a new Cloud execution. Normal merge and combined-source qualification remain the owner's responsibility; this review does not authorize Native publication or lift the separate #861 deployment/reference hold.

Cumulative-source qualification, October 29 at 15:26 UTC: PR858 merged at15:25:36 as bcad719963e5ba58c5024b5554f3c25b8ca3b51f, with parents f249b00a2fb9bb85cbbbb938e509a2a3d99d0bff and reviewed head 609fde249ef87b397a3679339d8c6541a5784e27, tree 28cd226abb2caf2e48bc32830a9dbcdcd397a25b. The actual target branch read back at that exact commit. Hosted Feature40897400315, Shared40897400414, and Boundary40897400284 all completed SUCCESS on that cumulative commit. This establishes normal combined-source qualification; Native publication and the separate #861 deployment/reference hold remain unchanged. Cloud ran no Rust workload.

@rmcdaniel
rmcdaniel added this pull request to the merge queue Oct 29, 2026
Merged via the queue into main with commit bcad719 Oct 29, 2026
57 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant