Repository navigation
api/client.rs downloads a served artifact through two near-copy GETs and hand-writes four retry loops under two retry policies #1869
Description
[agent] Filed by the scheduled architecture audit (audit-core). Register: register.
Kind: refactor. Register ID: A27.
Problem
1. Two GETs of a grant-tokenized serve URL.
download_vendor_archive_onceL1683-L1776 is behind the vendor archive and the crate-internaldownload_artifact(url)(gem stub gemspec).download_artifact_capped_onceL2851-L2937 is behindPatchApi::download_artifact(url, max_bytes), the hosted and in-memory engine.
Both do the same steps: the scheme check, the send timeout, 404/410 → miss, 408 → still building, the retry hint plus classify_auth_error, then read_capped_typed under the body timeout. They have drifted:
vendor archive / download_artifact |
PatchApi::download_artifact |
|
|---|---|---|
| other status | vendor package download failed with status N: <body> |
artifact download failed with status N (no body) |
| size-cap breach | ApiError::Network(..) |
ApiError::Other("artifact exceeds the N-byte limit") |
| transport text | Network error fetching vendor package: .. |
Network error fetching artifact: .. |
The two functions share the name download_artifact, but a cap breach comes back as a different error variant from each.
2. Four hand-written retry loops. They are send_json_request L567, request_vendor_references L1532, download_vendor_archive_from L1642 and download_artifact_capped L2827. The last three have the same shape: loop { match once() { (Err, Some(after)) if attempt < attempts => backoff; attempt += 1 } }.
The archive download also sits behind a chain of one-line wrappers: download_vendor_archive → download_vendor_archive_retrying → download_vendor_archive_from(url, 1).
3. Two retry policies that disagree. One is ApiRetryPolicy, for the JSON calls. The other is VendorRetryPolicy, for the vendoring service and artifact downloads.
ApiRetryPolicy (JSON) |
VendorRetryPolicy (vendor/artifact) |
|
|---|---|---|
| retried statuses | 429, 503 (is_retryable_status) |
429, 500, 502, 503, 504 (vendor_status_retryable) |
| transport | only a connection dropped mid-handshake | every transport error |
| count | max_retries: 3 (4 requests) |
attempts: 3 (3 requests) |
| jitter | equal jitter [d/2, d) |
±25% |
Retry-After over the cap |
gives up at once (a retry would be refused again) | clamped to max_delay: retries after 4 s |
| run-wide window | 60 s | none |
Executed on main @ 5ca5623, both tests pass:
api::retry::tests::delay_caps_retry_after_and_jitters_backoff_into_the_upper_halfpinsdelay(1, Some(120 s)) == None;api::client::vendor_retry_tests::delay_is_exponential_jittered_and_cappedpinsdelay(1, Some(60 s)) == 4 s.
So a vendoring-service 429 that says Retry-After: 60 is retried 4 s later. The JSON path's own module doc says such a retry "would only be refused again". The JSON path and the vendor path are retry loops in the same file that give opposite answers here.
Proposed change
- Use one
serve_get_once(url, max_bytes, noun)for both GETs, and map its outcome once. Pick one error variant for a cap breach.ApiError::Otheris whatPatchApicallers see today, and the archive path does not retry a cap breach either, so the variant change is not observable in retries. - Use one generic
retry_attempts(policy, key, once)helper for the three vendor loops, and deletedownload_vendor_archive,download_vendor_archive_retryingandvendor_download_retry. - Leave the policy values alone in this PR. The
Retry-After-over-cap rule is a behavior change; propose it in the PR description, or file it as a follow-up.
Size
1 file (api/client.rs), about −150 lines.
Acceptance criteria
- Behavior is unchanged apart from the unified error text and variant named in step 1. Update the tests that pin the old text in the same PR.
vendor_retry_tests::*,trait_download_enforces_the_callers_byte_cap,download_artifact_*,first_attempt_settles_or_defers_like_download_artifact,resuming_a_deferral_spends_one_budget_not_two,tests/api_retry_e2e.rsandtests/api_timeout_e2e.rspass.download_artifact_first_attempt/download_artifact_resumingstill spend one budget across a split.
Activity
audit-core: api/ and cargo_crawler (10 files, 13.7K lines); file #1867-…
mikolalysenko commentedon Oct 30, 2026
[agent] Claiming this for the architecture refactor routine (top eligible structural candidate, score ~7.5;
api/client.rsis touched by no open PR). Branch: arch-refactor/1869-serve-get-retry. Claim-ID: 2026-10-30T00:55:41Z-a27c1fGenerated by Claude Code
mikolalysenko commentedon Oct 30, 2026
[agent] Triage:
priority:p3(refactor). Addressed by open PR #1870; leaving this open until it merges.Generated by Claude Code