Rust CLI: match Node's API-error stderr shape; scope x402 hints to payments - #333
Merged
Conversation
A live Node-vs-Rust differential against staging surfaced two divergences.
The Node CLI unwraps the response envelope and prints the inner error
object to stderr ({code, message, ...}); the Rust port printed the raw
{error: {...}, success: false} envelope on every 4xx/5xx, breaking any
script that parses stderr and reads .code. error_message_for_status now
unwraps a non-null `error` field before pretty-printing. Exception,
matching Node: the payments path deliberately keeps the whole envelope
(payments-shared.ts documents it as the most useful payload for x402
flows), via a raw variant.
The Rust port also mapped the feature_disabled code to the "x402 payments
are not enabled" hint globally, so wake commands on a non-wake org got a
misleading payments hint. The x402 hint table now sits behind
error_for_status_with_payment_hints, used only by payments.rs; other
commands keep the generic unauthorized hint, like Node's ERROR_CODE_HINTS.
The parity fixtures never caught the shape divergence because all 4xx/5xx
cases asserted contains substrings that both shapes satisfy. Added three
cases pinning the exact shapes: generated-command flatten and
wake-without-hint with full Node<->Rust cross-compare; payments
envelope+hint per-runner (compare off only for JSON key order: Node
preserves server order, serde_json sorts, and the trailing hint line keeps
the stream from being JSON-normalized by the harness).
Validation: make rust-cli-check, cli-parity (456 cases); live staging
re-check shows byte-identical stderr for wake, emails, and invalid-key
errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Confidence Score: 5/5This looks safe to merge.
|
| Filename | Overview |
|---|---|
| cli-rust/src/client.rs | Splits generic and payment error formatting so generic errors flatten while payment errors keep the envelope. |
| cli-rust/src/payments.rs | Routes payment HTTP error handling through the payment-specific formatter. |
| test-fixtures/cli-parity/cases.json | Adds parity coverage for flattened generic errors, wake errors without x402 hints, and payment errors with the x402 hint. |
| cli-rust/tests/api_commands.rs | Adds bounded blocking reads to the generated API test server helper. |
| cli-rust/tests/config.rs | Adds bounded blocking reads to the OAuth refresh test server helper. |
| cli-rust/tests/functions_commands.rs | Adds bounded blocking reads to the functions command test server helper. |
| cli-rust/tests/payloads_auth.rs | Adds bounded blocking reads to the payload auth test server helper. |
Reviews (5): Last reviewed commit: "Merge updated rust-cli-port base" | Re-trigger Greptile
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts: # .github/workflows/rust-cli-release.yml # .github/workflows/sdk-checks.yml # Makefile # cli-rust/src/auth_commands.rs # cli-rust/src/client.rs # cli-rust/src/completion_commands.rs # cli-rust/src/payments.rs # cli-rust/tests/auth_commands.rs # scripts/run-cli-help-sweep.mjs # scripts/run-cli-parity.mjs # test-fixtures/cli-parity/cases.json
The accepted stream inherits the listener's nonblocking mode on macOS, so an early read races the client and panics with EWOULDBLOCK (os error 35), seen intermittently in CI (redeploy_wait_failed / deploy_wait_failed on macos-x64 and macos-arm64). Force the accepted stream back to blocking with a bounded read timeout in all four test servers that use the nonblocking-accept pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
etbyrd
force-pushed
the
rust-cli-port
branch
5 times, most recently
from
July 21, 2026 17:25
322c5b5 to
0765fa5
Compare
# Conflicts: # .github/workflows/rust-cli-release.yml # .github/workflows/sdk-checks.yml # Makefile # cli-rust/src/client.rs # cli-rust/src/completion_commands.rs # cli-rust/src/friendly.rs # cli-rust/src/functions_commands.rs # cli-rust/src/help_snapshots.generated.rs # cli-rust/src/payments.rs # cli-rust/tests/api_commands.rs # cli-rust/tests/completion_commands.rs # cli-rust/tests/config.rs # cli-rust/tests/functions_commands.rs # cli-rust/tests/payloads_auth.rs # scripts/generate-rust-cli-help-snapshots.mjs # scripts/run-cli-help-sweep.mjs # scripts/run-cli-parity.mjs # test-fixtures/cli-parity/cases.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two behavioral divergences found by a live Node-vs-Rust differential against staging (same API key through both CLIs, byte-compared output; 12/17 read-only commands were already identical):
{"code": ..., "message": ...}, seeextractErrorPayload/formatErrorPayloadinapi-command.ts); the Rust port printed the raw envelope ({"error": {...}, "success": false}). Scripts that parse stderr and read.codebreak when switching CLIs. The parity fixtures never caught it because every 4xx/5xx case assertedcontainssubstrings, which both shapes satisfy.feature_disablederror code to the "x402 payments are not enabled" hint globally, so e.g.wake schedules liston a non-wake org got a payments hint. The Node CLI scopes that hint table to payments commands (payments-shared.ts).What changed
client.rs::error_message_for_statusnow unwraps a non-nullerrorfield before pretty-printing, matching Node.error_for_status_with_payment_hints, used only by the threepayments.rscall sites. All other friendly commands keep the generic (unauthorized) hint only.payments-shared.tsdocuments the envelope as the most useful payload for x402 flows), so the payments variant uses the unflattened form.compare: falseonly because Node preserves server key order while serde_json sorts keys, and the trailing hint line keeps the stream from being JSON-normalized).Note for a possible follow-up
The key-order difference above is the last obstacle to byte-identical error output: enabling serde_json's
preserve_orderfeature would make Rust emit fields in server order like Node everywhere. Left out of this PR since it changes output ordering globally.Validation
make rust-cli-check(fmt, clippy-D warnings, all tests)make cli-parity: 456/456 cases green including the three new exact-shape caseswake schedules list,emails:get-email(bad id), and invalid-key errors now byte-match Node's stderr; payments envelope+hint retained🤖 Generated with Claude Code