Skip to content

feat!: BoxedHttpClient — an erased HTTP transport; Client<Ext> defaults to it - #2401

Merged
gold-silver-copper merged 4 commits into
mainfrom
bevy-prep/boxed-http-client
Aug 22, 2026
Merged

feat!: BoxedHttpClient — an erased HTTP transport; Client<Ext> defaults to it#2401
gold-silver-copper merged 4 commits into
mainfrom
bevy-prep/boxed-http-client

Conversation

@gold-silver-copper

Copy link
Copy Markdown
Contributor

Bevy-prep follow-up (after #2394 #2395 #2396 #2397 #2398 #2399 #2400); PR 8 of the data-layer series.

What

HttpClientExt is generic at the method level, so it is not object-safe and every client that holds a transport is generic over H. That is right for a library; a host that owns one transport for many providers (a worker pool, an ECS resource, a plugin) does not want H in every type it holds. This adds the erasure once, at the boundary:

  • rig_core::http_client::BoxedHttpClientArc<dyn ErasedHttpClient> where the private mirror collapses the generics to Bytes (the only bounds are Into<Bytes>/From<Bytes>); a blanket impl erases any H: HttpClientExt + 'static; BoxedHttpClient re-implements HttpClientExt. Byte-transparent; Clone is an Arc bump; boxing a boxed client is a clone, not a second layer; Debug prints only the type name (the inner transport may carry credentials); compile_fail-pinned non-Serialize.
  • Client<Ext, H = BoxedHttpClient> — the type default moves off the Missing typestate placeholder, so Client<Ext> in type position means "any transport". Expression-position inference is unchanged.
  • ProviderFromEnv::{from_env_boxed, from_val_boxed}, Client::boxed(self), ReqwestClient::boxed() / From<ReqwestClient> for BoxedHttpClient.
  • examples/agent_no_tokio now holds the transport erased.
  • Static Send + Sync + 'static pin for BoxedHttpClient.

The one break

Client::<Ext>::builder() no longer resolves (builder() lives on Client<Ext, Missing>): spell it Client::<Ext, Missing>::builder(), or go through a provider ClientBuilder / the rig-reqwest prelude as before. Documented in MIGRATING.md.

Why this lands before the rest of the data-layer series

Every later PR constructs or holds clients (a model registry populated at startup, spec resolution, a Bevy plugin's single transport resource). Fixing the boundary first means they are written against Client<Ext> once instead of H-generic signatures that get re-touched.

Verification

  • tests/providers/{anthropic,openai}/cassette/boxed_transport.rs: the recorded agent/completion_smoke and streaming/streaming_smoke scenarios replayed through Client<Ext, BoxedHttpClient> over the same bundled transport. The replay server matches on body bytes, so this is the byte-transparency proof. Both binaries pass whole (420 + 420).
  • Unit tests in http_client/erased.rs (pass-through of URI/headers/body, body conversion, error pass-through, clone identity, Debug redaction).
  • cargo check --workspace --all-features --all-targets, clippy, fmt, RUSTDOCFLAGS=-D warnings cargo doc, rig-core lib+doc tests, rig-agent, rig-reqwest, facade core (incl. the dependency-graph guard) — clean.
  • wasm32-unknown-unknown: rig-core (default and --all-features), rig-reqwest, rig-agent, rig — clean.
  • cargo tree -p agent_no_tokio -e normal | grep -E '^(tokio|reqwest)' unchanged from feat(agent): run_channel/RunEvents, static Send+Sync pins, bevy_tasks example, dependency-graph guard #2399: both enter only via rig-reqwest.

…pClientExt

A private ErasedHttpClient mirror collapses the method generics to Bytes
(the only bounds are Into<Bytes>/From<Bytes>), a blanket impl erases any
H: HttpClientExt + 'static, and BoxedHttpClient re-exposes the generic
surface on top. Byte-transparent; Clone is an Arc bump; boxing a boxed
client is a clone; Debug never prints the inner transport.
…_env_boxed, from_val_boxed}, Client::boxed

The type default moves off the Missing typestate placeholder onto the
erased transport, so Client<Ext> in type position means 'any transport'.
Expression-position inference is unchanged; the one break is
Client::<Ext>::builder(), which now needs Client::<Ext, Missing>::builder().
…ransport; anthropic/openai cassettes replayed through BoxedHttpClient

The replay server matches on body bytes, so a recorded scenario passing
through Client<Ext, BoxedHttpClient> over the same transport is the
byte-transparency proof.
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