fetch-kit
Resilient HTTP client for Rust — retry, circuit breaker, connection pooling, and typed JSON helpers built on reqwest, with a native middleware stack that mirrors the reqwest-middleware shape.
Naming: previously
fetchkit; renamed tofetch-kit(estate convention) becausefetchkiton crates.io belongs to an unrelated project (maintained byeverruns). Published asfetch-kit.
Features
- Native middleware stack —
Middlewaretrait +ClientWithMiddlewarechain with reqwest-middleware-compatible signatures, no extra dependency - Automatic retries with exponential backoff + jitter (estate
loop-retrysemantics) - Typed JSON helpers —
get_json/post_jsondeserialize responses directly - Fallback fetching — try a primary URL, fall back to another on failure
- Circuit breaker (opt-in) — stop hammering a dead service
- Bundled middleware — throttle (per-host token bucket), per-request timeout, base-URL prefixing
- Connection pooling — inherits reqwest's connection pool
Quick Start
use Client;
use Duration;
async
Middleware
fetch_kit::middleware::Middleware is the composition point. The trait
shape mirrors reqwest-middleware 0.5, so migrating an existing
middleware is an import-path swap:
use async_trait;
use ;
use Extensions;
use ;
Build a chain with middleware::ClientBuilder (wrapping any
reqwest::Client) or register middleware on the default stack with
ClientBuilder::with_middleware (user middleware run outermost — see
below):
use ClientBuilder;
let client = new
.with_middleware
.build;
Ordering semantics
Middleware wrap the transport in an onion. The middleware registered first is the outermost layer — it observes the request first and the response last:
ClientBuilder::new(client)
.with(AuthMiddleware) // 1st registered → outermost
.with(RetryMiddleware) // 2nd registered
.build();
request → Auth → Retry → transport
response ← Auth ← Retry ← ╹
Inner middleware therefore see every attempt an outer retry-style
middleware issues; outer middleware see only the final outcome. The
Next::run continuation passes the (possibly rewritten) request inward;
returning without calling it short-circuits everything below.
Bundled middleware
The core trait and chain are always available; each bundled implementation sits behind its own feature:
| Middleware | Feature | Behavior |
|---|---|---|
RetryMiddleware |
retry (default-on) |
Retries transport timeouts/connect errors and transient statuses (408, 429, 5xx except 501) with loop-retry exponential backoff + 10% jitter. Exhausted retryable statuses return the final response unchanged. |
CircuitBreakerMiddleware |
circuit-breaker |
Short-circuits with FetchError::CircuitOpen when the estate breaker is open; records every attempt. |
ThrottleMiddleware |
throttle |
Per-authority token bucket: sustained throughput never exceeds rate req/s, bursts up to burst_capacity. |
TimeoutMiddleware |
timeout |
Bounds each request (and everything downstream of it) to a wall-clock duration; surfaces FetchError::Timeout(d). |
BaseUrlMiddleware |
base-url |
Prefixes a base URL onto relative requests (marker host fetch-kit.relative, built via BaseUrlMiddleware::relative). |
Built-ins are defaults; middleware are composition points
The ClientBuilder knobs configure the built-in stack — retries /
retry_bounds shape the default retry middleware, with_breaker inserts
the breaker, base_url resolves paths eagerly. The default composition
(ordering: first = outermost) is:
[user middleware…] → retry → breaker → transport
- User middleware wrap the built-ins (registered via
ClientBuilder::with_middleware): they observe each logical request once, with final outcomes. - The breaker sits inside the retry loop: every HTTP attempt records
individually, so a flaky endpoint that burns retries trips the breaker
that much sooner — and a short-circuit (
CircuitOpen) is a middleware error, which retries never mask.
This is fetch-kit's differentiator over bare reqwest-middleware: the
resilience stack works out of the box, and the same pieces are available
as middleware when you want custom composition (e.g. throttle outside
retry, timeout inside it).
Request-scoped extensions
Carry typed, per-request data through the middleware chain (and across
retry attempts) with RequestBuilder::with_extension:
;
let value: Value = client
.get
.with_extension
.json_response
.await?;
// inside Middleware::handle: extensions.get::<RequestId>()
Composing at the middleware level
use ;
use Duration;
let raw = new
.with // outermost
.with
.with
.build;
Circuit Breaker
Enable the circuit-breaker feature to wrap requests in a breaker that
automatically rejects calls after repeated failures.
use Client;
use ;
async
Request Builder
use Client;
use Duration;
async
Migrating from 0.1.x
- Your own middleware: the trait shape is unchanged — swap
use reqwest_middleware::{Middleware, Next, Error, Result}foruse fetch_kit::middleware::{Middleware, Next, Error, Result}. Client::inner()now returns&fetch_kit::middleware::ClientWithMiddleware(fetch-kit's own chain, not reqwest-middleware's).from_partstakes that type.- Request-scoped data moves from request fields to the
extensionsargument ofMiddleware::handle(populated viaRequestBuilder::with_extension) — this matches howreqwest-middlewarepasses extensions, since reqwest does not expose request extensions publicly. - Backoff curve: retries now use the estate
loop-retrytiming (exponential, ×2 growth, ≤10% jitter) instead ofreqwest-retry's full jitter;retries/retry_boundssemantics are unchanged. - The
retryfeature (default-on) gates the built-in retry; disable default features to drop it.
Comparison with raw reqwest
| raw reqwest | fetch-kit | |
|---|---|---|
| Retries | manual or separate middleware | built-in (configurable) or RetryMiddleware |
| Middleware | none (tower only) | Middleware trait, reqwest-middleware shape |
| Throttling | write it yourself | ThrottleMiddleware (per-host token bucket) |
| JSON helpers | resp.json::<T>() each time |
get_json / post_json |
| Fallback | write it yourself | fetch_with_fallback |
| Timeout | per-client or per-request | builder default (30s) + TimeoutMiddleware |
License
Licensed under either of MIT or Apache-2.0 at your option.