fix(http): return an error when the HTTP client cannot be built - #2273
Open
jbmusso wants to merge 2 commits into
Open
fix(http): return an error when the HTTP client cannot be built#2273jbmusso wants to merge 2 commits into
jbmusso wants to merge 2 commits into
Conversation
`shared_http_client` panics when `reqwest::ClientBuilder::build` fails. Release builds set `panic = "abort"`, so that is not an unwind a caller can recover from: the process dies with SIGABRT and destructors never run. Callers already treat "no HTTP client" as a handled outcome, so the failure has to reach them as a value. The trigger seen in the wild is an image with no CA bundle, where the platform verifier reports "No CA certificates were loaded from the system". That is not reproducible portably, so the test drives the same failure branch through the documented `SSL_CERT_FILE` contract with a PEM block whose body is not valid DER. This test fails on the current implementation.
✅ Deploy Preview for viteplus-preview canceled.
|
`shared_http_client` panicked when `reqwest::ClientBuilder::build` failed. Because `[profile.release]` sets `panic = "abort"`, release builds turn that into SIGABRT: the process is gone before any caller can react, and destructors (download temp-dir guards, lock files) never run, and that was the very thing the panic was documented to preserve. Every failure at this point is an environment condition, not a vp bug: no system CA bundle, an unusable proxy, an unparsable extra CA bundle. Return `HttpClientError` instead and let the six call sites `?` it into the errors they already return, so each caller decides. The message names the cause and the remedy rather than asking for a bug report. Most visible in container images with no `ca-certificates` package, where the platform trust store loads zero roots. Node keeps its own root list compiled in, so `node` and `npm` still work there and nothing else reveals the missing bundle. Any direct subcommand (`fmt`, `lint`, `check`) reaches this path when the project pins a `packageManager` that is not in the managed cache yet, even though the download failure is already handled and the command would otherwise finish fine.
jbmusso
force-pushed
the
fix/http-client-empty-trust-store
branch
from
July 29, 2026 21:48
63141ff to
58c848c
Compare
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.
Problem
This surfaced while upgrading vite-plus from 0.1.21 to 0.2.6 in a CI pipeline running on
node:24-bookworm-slim: the lint and format jobs started aborting with exit 134, having been green on 0.1.21 on the same image.The branch is two commits, so the bug can be inspected before the fix: check out the first commit and run
cargo test -p vite_shared --lib client_build_failureto see the failure as a failing test, on an otherwise unmodified tree. The second commit is an attempt at a fix.shared_http_clientpanics whenever the client cannot be built, and every input funnels into that onepanic!: no system CA bundle, a malformed proxy setting, an unusable TLS backend, an unparsable extra CA bundle. Its doc comment states that a panic was chosen overprocess::exitso that:Cargo.tomlhas, under[profile.release]:abortdoes not unwind, so in every release build the panic delivers neither property it was chosen for: lockfiles and temp dirs are not cleaned, and SIGABRT takes the embedding Node host down with it. The comment and the release profile have drifted apart, and every one of those inputs becomes an unrecoverable abort, worst in exactly the NAPI-embedded case the comment was written to protect.The input that is near-universal in Debian-slim Node images is the missing CA bundle: they ship no
ca-certificatespackage, and Node carries its own root list, sonodeandnpmkeep working and nothing else reveals it. On such an image any directvpsubcommand aborts instead of running:Reproduces in about 30s with only Docker:
Holding image and project constant and varying only the vp version, 0.1.22 exits 1 with a handled error and 0.1.23 is the first release that aborts, the release that shipped the shared client from #1686. The empty trust store itself is not new (it has mattered since #1068 moved verification onto the OS store to fix #1014); what changed is that it became fatal.
Mechanism:
execute_direct_subcommandcallsenvs_with_explicit_package_manager_pathfor every direct subcommand. With apackageManagerfield and a cold cache that downloads the pinned package manager, which builds the shared client. Nothing here actually needs the network, since the download failure is already handled and falls back to the unmodifiedPATH, so with any single root certificate present the same command completes normally even under--network none. Without apackageManagerfield there is no download attempt and no crash, which is why this only shows up on projects that pin one.Solution
shared_http_clientnow returnsResult<&Client, HttpClientError>, and the six call sites?it into the errors they already return. That restores the behaviour the doc comment describes rather than adding a new policy: callers already treat "no HTTP client" as a handled outcome, they just never got the chance to. Every failure at that point is an environment condition (no system CA bundle, an unusable proxy, an unparsable extra CA bundle), so the message names the cause and the remedy instead of asking for a bug report, which currently points away from the fix.The two artifacts cover different things, deliberately. The Docker snippet above reproduces the reported symptom; the unit test pins the branch. I could not reproduce the empty trust store portably in a unit test, because macOS resolves roots through Security.framework and
SSL_CERT_FILEcannot empty the store there, so the test drives the samebuild()failure branch through the documentedSSL_CERT_FILEcontract, using a PEM block whose body is not valid DER. To check the real condition I built the patchedvite_sharedin a Debian container and deleted its CA store: it now returns the error and exits 0 instead of aborting.I only looked at this one call site, where the doc comment states the intent explicitly, so I can't say whether the same drift between a documented panic rationale and
panic = "abort"exists anywhere else. You may want to check.Note
The investigation and this patch were done with AI assistance, and my own Rust is limited, so please scrutinise the implementation closely. The diagnosis is empirically verified (bisected across releases, reproduced in a container), but I have lower confidence in the patch being the shape you would want. Happy to rework it however you prefer, or to leave the diagnosis with you if you would rather fix it yourselves.