Skip to content

fix(cli): say why a request got no response, and what to check - #90

Merged
thebrandonlucas merged 1 commit into
fix/cli-output-file-existsfrom
fix/cli-transport-error-cause
Oct 22, 2026
Merged

thebrandonlucas merged 1 commit into
fix/cli-output-file-existsfrom
fix/cli-transport-error-cause

Conversation

@thebrandonlucas

@thebrandonlucas thebrandonlucas commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

A request that gets no HTTP response now says why it failed, and a hint names the next step. Before, every failure read the same way, for example "HTTP read request failed. Check the API URL and whether the API service is running." The underlying error was dropped.

Class How it's detected Message ends with Hint
Timeout reqwest::Error::is_timeout no response within --timeout; for the auth service, no response within 30 seconds Retry, or pass a larger --timeout. For the auth service: retry, and its fixed 30-second limit is one that --timeout does not change.
Connect timeout is_timeout and is_connect could not connect within 10 seconds Check the URL's host, your network or firewall, and whether the service is reachable. --timeout does not change this connect limit.
TLS a rustls::Error anywhere in the cause chain, including inside nested io::Errors the TLS connection failed, for example because the server's certificate was not trusted Check the URL's host, and that this system trusts its certificate, including any proxy that inspects TLS.
Connect is_connect: DNS, a refused port or a proxy could not connect Check the URL, your network or proxy, and whether the service is running.
Other anything else, such as the connection closing before a response the connection closed before a response arrived Check the URL and whether the service is running.

This covers the API (reads, writes and the checkout event stream), the auth service and the price service. Each caller says which response limit applied: the API and price service use --timeout, and the auth service uses its fixed 30 seconds (AUTH_TIMEOUT). Each keeps its own consequence text: a write still says "a write may have been submitted. No mutation was retried." and keeps its unknown-outcome error.detail. The raw error text still never reaches the output, because it can contain hosts and paths.

Why: in the CLI benchmark, a sandbox blocked macOS trustd, so every TLS check failed in 19 of 23 CLI runs. The CLI said only "HTTP read request failed" while curl kept working, so agents switched to curl. Closes #84.

Stack: 3 of 6, on #88. It doesn't depend on #87 or #88.

  • User-visible change: a CHANGELOG.md (Unreleased) line is added, and docs/guides/troubleshooting.md (Exit code 4) describes the classes.
  • The verification gate passes: fmt, clippy, machete, cargo test (139 unit, 122 integration) and check-coverage.py.
  • Reviewed against fix(cli): say why --output-file refused a path, and that nothing was sent #88. Unit tests run real loopback servers: a refused port (Connect), plain HTTP answering TLS (TLS), a silent server with a 100 ms timeout (Timeout), and a server that closes early (Other). A further test checks the message, the hint and that the port stays out of the message. An auth unit test runs auth_request against a silent server and checks that the timeout names the fixed 30 seconds and does not suggest --timeout. Manual check: login --no-browser --json --timeout 300 against a silent auth server exits 4 after 30 seconds with that message. An integration test runs each of the 5 call sites against a closed port and checks exit code 4, the message and the hint, and that a write keeps detail.outcome = unknown.
  • CRAP on e4f73a8: 625 functions measured, 0 at or above 20. The highest is wait_for_payment at 19.60. The new functions score: auth_request 6.0, TransportFailure::of, classify, what and next_step 2.0 each, no_response 1.0.
  • Checks not run: a real certificate-verification failure. The TLS test uses a handshake failure, which takes the same path (a rustls::Error). The DNS case is grouped under Connect and isn't tested on its own, because it would need a resolver.

@thebrandonlucas
thebrandonlucas added this pull request to stack #89 October 22, 2026 19:15
@thebrandonlucas
thebrandonlucas marked this pull request as ready for review October 22, 2026 19:31
@thebrandonlucas
thebrandonlucas force-pushed the fix/cli-transport-error-cause branch from 1684850 to 134f858 Compare October 22, 2026 20:33

@tee8z tee8z left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes on 134f858. The classification is right. The rustls check walks the nested io::Errors, a connect timeout is checked before a timeout, a write that may have been applied is never told to retry, and the raw error text stays out of the message. One finding:

P2: an auth-service timeout blames --timeout, which does not apply to it. The auth client is client(AUTH_TIMEOUT), a fixed 30 seconds (src/auth.rs:30, :227). --timeout reaches only the API client (api.rs:186) and the price service. Yet TransportFailure::Timeout always says no response within --timeout, and for a read its hint is Retry, or pass a larger --timeout. I reproduced this on this head against a silent auth server:

$ voltage login --no-browser --json --auth-url http://127.0.0.1:PORT --timeout 300
{"error":{"exit_code":4,"hint":"Retry, or pass a larger --timeout.","message":"Authentication request failed: no response within --timeout; no token request was automatically retried"}}

It exited after 30 seconds, not 300. An agent that follows the hint raises --timeout and gets the same failure. This PR already gives ConnectTimeout its own text for this reason. Let the caller say which limit applied, for example by passing the limit's description to no_response or a flag for a fixed limit, so that auth reports "within 30 seconds" and does not suggest --timeout. Please add a test for it.

Nonblocking: the description's table lists four classes and has no row for ConnectTimeout. The CHANGELOG and troubleshooting guide do describe it.

Validation on this head, in nix develop: cargo fmt --check, cargo clippy --locked --all-targets -- -D warnings, cargo machete --with-metadata, cargo test --locked (137 unit, 122 integration) and python3 scripts/check-coverage.py passed. CRAP: 623 measured functions, none at or above 20, highest wait_for_payment at 19.60; 176 functions were unmeasured. CI is green.

Comment thread src/error.rs Resolved
Comment thread src/auth.rs Resolved
@thebrandonlucas
thebrandonlucas force-pushed the fix/cli-transport-error-cause branch from 134f858 to e4f73a8 Compare October 22, 2026 21:14

@tee8z tee8z left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved e4f73a8 against fix/cli-output-file-exists (#88 78c8a98). My finding is fixed. ResponseLimit::Fixed(AUTH_TIMEOUT) is passed only from auth_request. The API, the checkout stream and the price service, whose clients are built from global.timeout, keep ResponseLimit::Timeout. A write that times out against a fixed limit still leads with the look-up-first step.

I repeated my reproduction on this head. voltage login --no-browser --json --timeout 300 against a silent auth server now exits after 30 seconds with Authentication request failed: no response within 30 seconds; no token request was automatically retried and the hint Retry. The auth service has a fixed 30-second limit, which --timeout does not change. The new unit test pins both.

Validation on this head, in nix develop: cargo fmt --check, cargo clippy --locked --all-targets -- -D warnings, cargo machete --with-metadata, cargo test --locked (139 unit, 122 integration) and python3 scripts/check-coverage.py passed. CRAP: 625 measured functions, none at or above 20, highest wait_for_payment at 19.60; 176 functions were unmeasured. When I approved, CI on x86_64 macOS was still pending, and the other jobs had passed.

@thebrandonlucas
thebrandonlucas merged commit f1a612f into master Oct 22, 2026
7 checks passed
@thebrandonlucas
thebrandonlucas deleted the fix/cli-transport-error-cause branch October 22, 2026 21:53
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.

Transport errors drop their cause; TLS, proxy and DNS failures all read as 'HTTP read request failed'

2 participants