Skip to content

chore(deps): move reqwest to 0.13 and adopt the fleet TLS-provider convention - #731

Merged
forkwright merged 7 commits into
mainfrom
chore/reqwest-0.13-tls-align
Aug 26, 2026
Merged

chore(deps): move reqwest to 0.13 and adopt the fleet TLS-provider convention#731
forkwright merged 7 commits into
mainfrom
chore/reqwest-0.13-tls-align

Conversation

@forkwright

Copy link
Copy Markdown
Owner

harmonia ran reqwest 0.12 with the rustls-tls feature while aletheia and
theatron ran 0.13 with rustls-no-provider. The version gap mattered less than
the feature: aletheia's manifest documents a fleet convention that the ring
crypto provider is installed once, explicitly, process-wide, and that no library
is ever allowed to link one implicitly. rustls-tls does exactly what that
forbids.

So this is two changes, and the second is the one that can hurt. Building with
rustls-no-provider means any process performing TLS must install a provider
itself, and failing to is a RUNTIME PANIC on first use rather than a compile
error -- it would pass CI and fail in the operator's hands.

Both binary targets are covered at their real entry points:

crates/archon/src/main.rs the harmonia binary, install added directly
crates/theatron/desktop/src/main.rs calls periskopio::run(), whose first
statement is the install

The eight library crates that depend on reqwest are deliberately untouched:
installing a provider from a library is the implicit linking the convention
exists to prevent.

form and query join the feature list because 0.13 moved them behind
features. periskopio now inherits reqwest from the workspace rather than
carrying its own pin, so the two cannot drift again.

Authored by an agent whose result was lost when this box thermally shut down
mid-run; the tree survived and every claim above was re-derived from it rather
than taken on trust. Not compiler-verified here -- harmonia is public, its
runner minutes are free, and this laptop shuts down under a workspace check.

forkwright added 2 commits August 24, 2026 14:26
…nvention

harmonia ran reqwest 0.12 with the `rustls-tls` feature while aletheia and
theatron ran 0.13 with `rustls-no-provider`. The version gap mattered less than
the feature: aletheia's manifest documents a fleet convention that the ring
crypto provider is installed once, explicitly, process-wide, and that no library
is ever allowed to link one implicitly. `rustls-tls` does exactly what that
forbids.

So this is two changes, and the second is the one that can hurt. Building with
`rustls-no-provider` means any process performing TLS must install a provider
itself, and failing to is a RUNTIME PANIC on first use rather than a compile
error -- it would pass CI and fail in the operator's hands.

Both binary targets are covered at their real entry points:

  crates/archon/src/main.rs          the `harmonia` binary, install added directly
  crates/theatron/desktop/src/main.rs  calls periskopio::run(), whose first
                                     statement is the install

The eight library crates that depend on reqwest are deliberately untouched:
installing a provider from a library is the implicit linking the convention
exists to prevent.

`form` and `query` join the feature list because 0.13 moved them behind
features. periskopio now inherits reqwest from the workspace rather than
carrying its own pin, so the two cannot drift again.

Authored by an agent whose result was lost when this box thermally shut down
mid-run; the tree survived and every claim above was re-derived from it rather
than taken on trust. Not compiler-verified here -- harmonia is public, its
runner minutes are free, and this laptop shuts down under a workspace check.
…cannot inherit

This branch had periskopio reading `reqwest.workspace = true` and
`rustls.workspace = true`. crates/theatron/desktop is in the ROOT manifest's
`exclude` list, not `members`, so it has no workspace to inherit from and cargo
refuses the manifest outright:

    error inheriting `reqwest` from workspace root manifest's
    `workspace.dependencies.reqwest`
    failed to find a workspace root

Declared explicitly instead, with a comment saying why, because this is the
second time today this exact exclusion has produced this exact error -- the
licence fix in #725 hit it first. A reader who does not know the crate is
excluded will reach for inheritance again.

Features match what periskopio declared before this branch -- json, plus
rustls-no-provider where it read rustls-tls. Not the root's fuller set: it never
used `form`, `query` or `stream`, and inheriting features a crate does not
exercise is how a dependency graph grows without anyone deciding to.

Verified the manifest now parses standalone via its own --manifest-path, which
is how CI checks it.
@forkwright

Copy link
Copy Markdown
Owner Author

Second blocker found: this is entangled with librqbit

The manifest-inheritance failure is fixed (an excluded crate cannot inherit — see the follow-up commit). The remaining cargo deny failure is not mine to fix here:

error[duplicate]: found 2 duplicate entries for crate 'reqwest'
error[duplicate]: found 2 duplicate entries for crate 'wasm-streams'

The lockfile carries reqwest 0.12.28 and 0.13.4, and cargo tree -i names the holder:

reqwest v0.12.28
├── librqbit v8.1.1
│   [dev-dependencies]
├── librqbit-tracker-comms v3.0.0
└── librqbit-upnp v1.0.0

librqbit 8.1.1 pins reqwest 0.12. Moving the workspace to 0.13 therefore produces exactly the duplicate this repo's deny.toml bans — the bump cannot be clean while librqbit 8 is in the graph.

The entanglement runs both ways

#724 proposes librqbit 8.1.1 → 9.0.1, and a prior read of its CI found that bump also reports duplicate-version violations, naming the same crates: atoi, reqwest, wasm-streams. So neither the reqwest bump alone nor the librqbit bump alone is clean, and #724 additionally needs a real source migration — SessionOptions fields, PersistentDhtConfig, TorrentStatsState::Initializing, TorrentMetadata.name, plus arg-count and method breaks.

I have not established which reqwest librqbit 9.0.1 requires. crates.io's dependency API refuses unauthenticated reads, and guessing the version that would make the graph converge is exactly the kind of assumption that turns a blocked PR into a wrong one. Whoever does the librqbit migration will have that answer from the resolver directly.

Disposition

Left open. The TLS-provider alignment in this PR is correct and worth keeping — it is the fleet convention, both binaries install the ring provider at their real entry points, and that part is independent of the version question.

What it cannot do is land while librqbit holds reqwest 0.12. These two PRs need sequencing, and #724 is the one carrying the actual work.

forkwright pushed a commit that referenced this pull request Aug 25, 2026
…three dead advisories

librqbit 9 widens the graph and cargo-deny's multiple-versions rule refuses it.
Three duplicates, and only one is genuinely ours to exempt.

atoi is upstream-blocked in both directions: sqlx-sqlite's newest release still
pins 2.0 and librqbit 9 pins 3. cargo update cannot close a gap where both sides
are already newest and disagree on a major, so it is skipped with the specific
crates and versions named.

reqwest is deliberately NOT skipped. Our own manifest caps us below 0.13, and
lifting that cap is #731's core change -- so this unifies by sequencing rather
than by exemption. wasm-streams is purely downstream of reqwest and needs no
action of its own.

Three advisory exceptions are dead and removed. bincode is entirely absent from
this branch's lock where main carried two versions, so RUSTSEC-2025-0141 and its
two skip entries go. Checking that turned up the same shape in quick-xml: main
had two versions, this branch has one because librqbit-upnp no longer pins the
old range, so RUSTSEC-2026-0194 and -0195 go with it. A comment on an unrelated
syn entry that referred back to bincode is corrected rather than left dangling.

The derived ignore files are regenerated through the tool their own header names
as the source of truth, not hand-edited, and check clean against deny.toml.
forkwright added 3 commits August 25, 2026 17:15
reqwest 0.13 requires a process-level crypto provider before a Client can be
built, and this branch installs one at each real binary entry point. Test
binaries never call main(), so nextest ran every one of them without it and the
first client build panicked:

  No rustls crypto provider is configured... you must install a crypto provider
  before building a Client

nextest then cancelled with three tests still running, so the reported blast
radius was the count before the abort rather than the set.

Swept the workspace for Client::new, Client::builder and reqwest::get -- 88 call
sites across 29 files -- and covered all of them. Where a crate already funnels
its tests through a shared spawn helper the install goes there, one call covering
roughly seventy sites; the rest are fixed at the site because they build a client
without the fixture. The pattern follows aletheia's existing
organon::testing::install_crypto_provider rather than inventing a new one, and
install_default is the only lever: reqwest 0.13.4 has no per-call override.

Worth knowing for review: `.unwrap_or_default()` does NOT catch this. It is a
real panic, not a Result::Err, so nine production sites carry a comment saying
the build "fails only with invalid TLS config" that is now wrong. Harmless today
because main() installs first; left alone rather than widening this change.

The three ambiguous rustls builder sites are fixed here too. They do not panic on
this branch -- aws-lc-rs is absent from its lock -- but librqbit 9 reintroduces
the ambiguity through reqwest's redefined default-tls, so fixing them now means
they are already right when that rebases, and avoids a conflict.

The reqwest and wasm-streams duplicates are skipped TEMPORARILY, labelled as such
and naming the PR that resolves them: our own manifest caps reqwest below 0.13,
so only this change can lift it, and the versions converge once librqbit 9
rebases on top. The skip should be deleted then rather than left standing.
…tests

The first sweep found every direct client construction and then misjudged some of
them. A site was marked "production, safe -- main() installs the provider" after
checking whether its OWN crate's tests reached it through a test seam. That check
is per-crate, and the reach that matters is not.

archon's supervisor test constructs epignosis's real ProviderBackedResolver by
name -- not a mock, not a seam -- so epignosis's production reqwest builder ran
inside a test binary that never called main(). A per-crate audit cannot see that;
the question has to be "does any test ANYWHERE in the workspace call this
constructor", asked across the whole tree.

Re-asked that way, seven more crates had reachable sites. Fixed at the choke
points where one exists -- epignosis's shared resolver fixture covers sixteen
sites, eksetasis's service builder covers twenty-three, archon's feed scheduler
covers four -- and per-site where no fixture funnels them.

Sites confirmed already covered are listed in the PR rather than touched: every
PlexClient and ByparrProxy test is preceded by a spawn helper that installs, and
ergasia reaches librqbit 8's separate reqwest instance, which this branch's
feature does not affect.

The nine wrong `.unwrap_or_default()` comments are corrected. They claimed the
build "fails only with invalid TLS config"; it panics, and unwrap_or_default
cannot intercept a panic. Each now says so and names the install that makes its
call site safe.
…ve client

Completes the sweep by enumeration rather than by waiting for CI to name the next
one. serve.rs has five test modules, not one; all 46 tests are now accounted for
individually -- constructor called, whether it can reach a client build, and why
not where it cannot.

Ten reach a client and were already covered. The eleventh,
zetesis_supervisor_tick_refreshes_stale_caps_and_skips_fresh, constructs
eksetasis::SearchIndexerService::new directly, at a second call site of a
constructor already fixed at its first.

That is the whole miss, and it is worth naming: the previous method -- grep each
named constructor across the workspace -- was right and did surface this line. It
was not followed to the end of its own results. Every constructor fixed in this
branch has now had its full grep result count reconciled.

Crates confirmed unable to reach a client are recorded rather than assumed:
kritike, kathodos, paroche and syntaxis declare no reqwest dependency, and
ergasia reaches librqbit 8's separate reqwest instance, which this branch's
feature does not affect.
@forkwright

Copy link
Copy Markdown
Owner Author

Note on why this took several rounds, and the mechanism that would end the class

Three rounds of "CI names one test, fix it, CI names the next". Worth recording, because the cause is not what it looks like.

nextest cancels on first failure, so every list CI produced was a sample, not a census. Treating each one as the full set guaranteed another round.

The final pass replaced that with enumeration: all 46 tests across serve.rs's five test modules, each with an explicit verdict on whether it can reach a client construction and why not where it cannot. Eleven reach one. Crates that structurally cannot are recorded rather than assumed — kritike, kathodos, paroche and syntaxis declare no reqwest dependency, and ergasia reaches librqbit 8's separate reqwest instance, unaffected by this branch's feature.

The last miss deserves naming honestly: the method — grep each named constructor across the whole workspace — was correct and did surface the failing line. It was not followed to the end of its own results; the first hit was fixed and a second, further down, was not checked. Every constructor touched in this branch has now had its full grep result count reconciled.

The automatic mechanism, evaluated and deliberately not taken

A Once/OnceLock wrapper adds nothing: install_default() is already safe to call repeatedly, and the failure was never "called unsafely twice" — it was "never called at all".

A true per-binary init needs ctor, and there is a real subtlety worth writing down: #[cfg(test)] code is per-crate-compilation. A #[ctor] declared in epignosis's test code would not have prevented either miss, because when archon depends on epignosis as a normal library, epignosis's test code — ctor included — is never compiled into archon's test binary. The fix has to live in whichever crate owns the test.

Where it would genuinely help is archon specifically: all five of its test modules compile into one binary, so a single #[ctor] would cover all 46 permanently, including tests written later that reach into another crate by a path no enumeration can anticipate.

Not done here. It introduces a new workspace dependency with no prior use in this codebase, and it is the kind of call that deserves an explicit decision rather than riding in on a "fix the named test" pass. Recorded so it is available if this class recurs.

forkwright added 2 commits August 25, 2026 18:40
…ared helper

Enumerated every test in all eight crates that depend on reqwest directly -- the
eighth, theatron/desktop, had never been checked and turns out to construct no
client at all.

The failure shape is not what the earlier rounds assumed. It is not that a test
forgot to spawn a mock server. It is that a shared local helper funnels most
tests through a safe path while a minority in the same file construct at the
site, bypassing the precondition entirely. cardigann has 52 tests behind a
three-tier wrapper and two that go straight to CardigannClient::new; the wrapper
is fixed once and both direct sites individually.

newznab and torznab each funnel every test through one client() helper, so those
are one call each. ByparrProxy::new is hardened at the constructor as
defence-in-depth even though its tests were already safe.

Worth recording because it nearly repeated the miss: syndesmos's plex/collections
and plex/stats contain ZERO literal "reqwest" and still construct a real
PlexClient at eight sites. A keyword sweep calls those files safe. All eight are
preceded by an installing spawn helper in the prior statement, so they need no
change -- but only reading each body establishes that.
Five CI rounds established that per-site installation does not converge here.
Each round the sweep was keyed on something -- a call pattern, a constructor
name, a crate boundary -- and each round some path did not use that key. The last
one reached a client through a registry entry point rather than
CardigannClient::new, so a careful count of that constructor was correct and
still missed it.

A constructor that runs at binary load removes the precondition instead of
satisfying it repeatedly. There is no longer a question of whether a given test's
construction path was preceded by the right call.

Every per-site install is deleted rather than left alongside it, including one in
search/tests.rs from an earlier round. Keeping both would leave a reader unable to
tell which is load-bearing, and the dead one rots.

The attribute form is read from the crate's live documentation, not recalled:
ctor 1.0 made `unsafe` mandatory inside the attribute, so bare `#[ctor]` is
rejected. The function body stays safe; the unsafe is on the attribute.

Scoped to this crate deliberately. `#[cfg(test)]` code is compiled per crate, so
this protects eksetasis's own tests and nothing else -- a constructor here would
never load into another crate's test binary. syndesmos, the only other crate that
has produced this bypass in the wild, is tracked separately in #737.
@forkwright
forkwright merged commit 1d6367e into main Aug 26, 2026
17 checks passed
@forkwright
forkwright deleted the chore/reqwest-0.13-tls-align branch August 26, 2026 00:02
forkwright pushed a commit that referenced this pull request Aug 26, 2026
…three dead advisories

librqbit 9 widens the graph and cargo-deny's multiple-versions rule refuses it.
Three duplicates, and only one is genuinely ours to exempt.

atoi is upstream-blocked in both directions: sqlx-sqlite's newest release still
pins 2.0 and librqbit 9 pins 3. cargo update cannot close a gap where both sides
are already newest and disagree on a major, so it is skipped with the specific
crates and versions named.

reqwest is deliberately NOT skipped. Our own manifest caps us below 0.13, and
lifting that cap is #731's core change -- so this unifies by sequencing rather
than by exemption. wasm-streams is purely downstream of reqwest and needs no
action of its own.

Three advisory exceptions are dead and removed. bincode is entirely absent from
this branch's lock where main carried two versions, so RUSTSEC-2025-0141 and its
two skip entries go. Checking that turned up the same shape in quick-xml: main
had two versions, this branch has one because librqbit-upnp no longer pins the
old range, so RUSTSEC-2026-0194 and -0195 go with it. A comment on an unrelated
syn entry that referred back to bincode is corrected rather than left dangling.

The derived ignore files are regenerated through the tool their own header names
as the source of truth, not hand-edited, and check clean against deny.toml.
forkwright pushed a commit that referenced this pull request Aug 26, 2026
Rebased onto #731. The prediction it carried holds: with archon on reqwest 0.13
and librqbit 9 resolving to 0.13.4, the two converge -- the lock now has ONE
reqwest entry and wasm-streams 0.4.2 drops out entirely.

So #731's temporary skips are removed rather than duplicated here, which is what
its own comment asked for: one exemption, removed once, not two. bincode goes
with them; it is absent from this lock, since librqbit 9 no longer pulls the 1.x
line.

atoi stays, and it is the only duplicate genuinely exempted: sqlx-sqlite pins 2.0
at its newest release and librqbit 9 pins 3, so no cargo update closes it.

The rustls provider fix is not re-applied -- #731 landed the same change in
archon and syndesis, and this branch takes main's version of both files.

The lock is regenerated by cargo rather than hand-merged through the rebase
conflict. Each side had resolved with the other's crate still present, so
combining them by hand would have produced a file cargo would not.
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