better-fetch
Typed HTTP client layer on top of reqwest, inspired by @better-fetch/fetch. Independent Rust implementation.
Installation
Pick one crate name (same library):
[]
= "0.2"
= { = "1", = ["derive"] }
= { = "1", = ["macros", "rt-multi-thread"] }
= "0.7"
Aliases on crates.io: typed-fetch, api-fetch — pub use better_fetch::*.
Optional features:
= { = "0.2", = ["tower", "validate", "multipart"] }
Quick start
use ;
use Deserialize;
async
Highlights
- Builder API —
Client::builder(), per-request.timeout(),.retry(),.auth(), headers, JSON body. - Retries — linear, exponential, or
count;Retry-After, jitter, customshould_retry; default retry on 408/429/502/503/504. - Hooks & plugins — compose client and plugin hooks; optional
LoggerPlugin(requires atracingsubscriber in your app). - Errors —
Result+?;Error::api_json()to parse JSON error bodies from APIs. - Typed endpoints —
Endpointtrait +client.call::<E>()→EndpointRequestBuilderwith typedsend_json(). - Testing — inject
ClientBuilder::backend(Arc<dyn HttpBackend>). - Cancellation —
CancellationTokenper request; cooperative abort during requests and retry backoff. - Throw mode —
throw_on_error(true)makessend()returnErron non-2xx (like upstreamthrow: true). - Form & multipart —
.form([...])for url-encoded bodies;.multipart(form)with featuremultipart.
Request options
| Method | Description |
|---|---|
.param / .params / .params_iter |
Path template :id substitution |
.query / .queries |
Query string (stable insertion order via IndexMap) |
.query_json |
Serialize a value into a query param (feature json) |
.json / .body |
Request body |
.form |
application/x-www-form-urlencoded body |
.multipart |
Multipart form (feature multipart) |
.timeout / .retry |
Per-request overrides |
.auth / .bearer_token |
Per-request auth |
.cancellation_token |
Cancel in-flight request + retry sleeps |
.throw_on_error |
send() returns Err on non-2xx when true |
.send / .send_json |
Execute request |
Cancellation
use ;
use Duration;
async
Throw on HTTP error
// Default: Ok(Response) even for 404
let response = client.get.send.await?;
// Like upstream throw: true
let err = client
.get
.throw_on_error
.send
.await
.unwrap_err;
Typed endpoint
use ;
use Method;
use Deserialize;
;
async
Form and multipart
// URL-encoded form
client
.post
.form
.send
.await?;
// Multipart (feature "multipart")
let form = new.text;
client.post.multipart.send.await?;
Note: automatic retry is not supported with .multipart() (the body cannot be replayed). Use .form, JSON, or raw bytes if you need retries.
Client builder
ClientBuilder::build() requires .base_url(...) — otherwise Error::MissingBaseUrl.
use ClientBuilder;
let client = new
.base_url?
.retry
.build?;
Concurrency limits
ClientBuilder::max_in_flight uses a tokio semaphore in the core client (counts retries as in-flight work). The tower feature’s ConcurrencyLimitLayer is a separate transport-level cap. Use one of these at a given limit unless you intentionally want two stacked caps (e.g. app-wide budget + per-host transport limit).
Plugins
Plugin::init receives PreparedRequest with url, path, method, and headers (after auth, before lifecycle hooks). Use it to rewrite URLs or inspect auth headers.
Features
| Feature | Description |
|---|---|
reqwest, json, rustls-tls (default) |
Async client, JSON, TLS |
native-tls |
Platform TLS |
blocking, cookies |
Passed through to reqwest |
multipart |
RequestBuilder::multipart + better_fetch::multipart re-export |
schema / openapi |
schemars registry, strict routes, and OpenAPI 3.0 export |
tower / tower-http |
Tower Service transport stack (better_fetch::tower) |
validate |
Response validation with garde (send_json_validated) |
macros |
Reserved better-fetch-macros crate |
See CHANGELOG.md for release notes.
Examples
Crates in this repository
| crates.io | Role |
|---|---|
| better-fetch | Main library |
| typed-fetch | Re-export alias |
| api-fetch | Re-export alias |
| better-fetch-macros | Proc macros (reserved) |
License
MIT — see LICENSE. Upstream inspiration: THIRD_PARTY_NOTICES.md.