A fast, safe HTTP client that works across runtimes and transports.
let response = client.get("https://example.com").fetch().await?;
assert!(response.status().is_success());HttpClient supports different async runtimes and transports. It includes secure defaults,
resilience, telemetry, and test utilities.
fetch includes the features that services commonly need:
- Secure TLS validation, retries, circuit breaking, hedging, and OpenTelemetry telemetry.
- Fake responses through the
test-utilfeature. - A request pipeline that supports custom logging, metrics, retries, and other handlers.
- Memory pooling and zero-copy operations for large responses.
fetch does not require one runtime, I/O implementation, or HTTP transport. A transport handler
performs I/O at the end of the request pipeline. Resilience, telemetry, routing, logging, and
retries run above that handler.
- Tokio works with the bundled transport. A custom transport can use another runtime and I/O implementation.
- A transport implements
RequestHandler. It can use the bundled hyper transport, a custom client, or another client such asreqwest.
A library can use fetch while the application selects the runtime and transport. See the
custom module and custom::create_builder for an example.
Both fetch and reqwest use
hyper by default. fetch also includes resilience and supports
custom runtimes and transports. A custom transport can use reqwest while the rest of the
fetch pipeline stays unchanged.
Note:
fetchuses methods such asget(),post(), andfetch(), which are similar toreqwestmethods.
The bundled transport runs on Tokio. Use a custom transport for another runtime. See the
custom module.
use fetch::{HttpClient, HttpError};
#[tokio::main]
async fn main() -> Result<(), HttpError> {
// Create a client with the bundled Tokio transport.
let client: HttpClient = HttpClient::new_tokio();
// Return an error for a `4xx` or `5xx` status, then read the body as text.
let body: String = client.get("https://example.com").fetch_text_body().await?;
println!("response: {body}");
Ok(())
}Note: Use
HttpClient::builder_tokioto add handlers or change timeouts and other configuration.
Use methods such as HttpClient::get and HttpClient::post for common requests. Each method
returns a builder for request-specific configuration.
// Send a GET request.
let response: HttpResponse = client.get("https://www.example.com").fetch().await?;// Send a POST request with a text body.
let response = client
.post("https://httpbin.org/post")
.text("the exact body that is sent")
.fetch()
.await?;The client has methods such as HttpClient::put and HttpClient::delete. Use
HttpClient::request for another HTTP method:
// Send a PATCH request.
let response = client
.request(Method::PATCH, "https://api.example.com/items/42")
.fetch()
.await?;A request can include headers, an HTTP version, and a body:
let response = client
.post("https://api.example.com/upload")
// Add HTTP headers.
.header(header::AUTHORIZATION, "Bearer token123")
.header(header::CONTENT_TYPE, "application/json")
// Select the HTTP version.
.version(Version::HTTP_2)
.text("{\"name\": \"document.pdf\"}")
.fetch()
.await?;These methods return HttpRequestBuilder, which configures and sends the request.
Use HttpClientBuilder::base_uri to set one BaseUri for all requests.
The client base URI overrides a base URI in a request.
let client = builder
.base_uri(BaseUri::from_static("https://example.com/api/v1/")) // The trailing slash is required.
.build();
// This request uses `https://example.com/api/v1/foo/bar`.
let response = client.get("/foo/bar").fetch().await?;HttpRequestBuilder::fetch returns an HttpResponse with the body, status code, headers,
and extensions. HttpResponse is a type alias for Response<HttpBody>.
A response supports these operations:
HttpResponse::ensure_successreturns an error if the status is not2xx.HttpResponse::statusreturns the HTTP status.HttpResponse::headersreturns the response headers.HttpResponse::into_bodyreturns the response body.HttpBody::into_textandHttpBody::into_bytesconvert the body.HttpBody::into_jsonconverts the body when thejsonfeature is enabled.
// Send a GET request.
let mut response: HttpResponse = client.get("https://www.example.com").fetch().await?;
// Return an error if the status is not successful.
response = response.ensure_success()?;
// Read the headers.
println!("Headers: {}", response.headers().len());
// Consume the response and return its body.
let body: HttpBody = response.into_body();
// Read the body as text.
let text: String = body.into_text().await?;
println!("Response body: {}", text);These methods fetch a response and convert its body:
fetch_textreturns a string.fetch_bytesreturns aBytesView.fetch_jsonreturns owned JSON and requiresjson.fetch_json_refreturns zero-copy JSON and requiresjson.
Each method returns Response<T>. The response retains its status, headers, and extensions.
// Return the response with a text body.
let response = client
.get("https://api.example.com/users")
.fetch_text()
.await?;
// Read response metadata before the body.
println!("Status: {}", response.status());
println!("Content-Type: {:?}", response.headers().get("content-type"));
// Return an error for an unsuccessful status, then extract the body.
let text: String = response.ensure_success()?.into_body();The _body variants call ensure_success, discard the
response metadata, and return the converted body.
fetch_text_bodyreturns aString.fetch_bytes_bodyreturns aBytesView.fetch_json_bodyreturns an owned value and requiresjson.
// Fetch the response, require a successful status, and return the text body.
let text: String = client
.get("https://api.example.com/users")
.fetch_text_body()
.await?;
println!("body: {text}");The HTTP client uses templated_uri for URLs. Uri supports dynamic path segments and
query parameters through templates.
Templates follow RFC 6570 level 3. Uri and
string types also support URLs without typed templates. In that case, the full PathAndQuery
string becomes the template.
handlers::Logging records the URL template as url.path.template.
This example creates a Uri with a templated path:
use templated_uri::{BaseUri, EscapedString, PathAndQueryTemplate, Uri, templated};
#[templated(template = "/users/{user_id}/", unredacted)]
#[derive(Clone)]
struct UserPath {
user_id: EscapedString, // EscapedString escapes the value for use in URIs.
}
let user_path = UserPath {
user_id: EscapedString::from_static("12345"),
};
client
.get(
Uri::default()
.with_base(BaseUri::from_static("https://api.example.com"))
.with_path_and_query(user_path),
)
.fetch_text()
.await?;templated_uri uses data_privacy to classify URL paths and queries. The classified attribute
lets a PathAndQueryTemplate use classified
data_privacy types.
use data_privacy::Sensitive;
use templated_uri::{EscapedString, PathAndQueryTemplate, templated};
#[templated(template = "/{org_id}/user/{user_id}")]
struct UserPath {
#[unredacted]
org_id: u32,
user_id: Sensitive<EscapedString>,
}The json feature supports these operations:
HttpRequestBuilder::jsonserializes a Rust type as JSON.HttpRequestBuilder::fetch_jsondeserializes into an owned Rust type. The value can outlive the response. It can cross thread boundaries if its type implementsSend.HttpRequestBuilder::fetch_json_refreturnsJson<T>. It can borrow strings and byte arrays from the response buffer.HttpBody::into_jsonconverts a response body into JSON.
Use fetch_json for owned data. It deserializes JSON directly into the target type:
// Define a type with owned data.
#[derive(Serialize, Deserialize)]
struct Person {
id: u32,
name: String,
}
let person = Person {
id: 1,
name: "Alice Johnson".to_owned(),
};
// Send and receive owned JSON.
let response: Response<Person> = client
.put("https://api.company.com/people")
.json(&person)
.fetch_json::<Person>()
.await?;
// Read the response metadata.
let response = response.ensure_success()?;
// Extract the deserialized value.
let person: Person = response.into_body();
println!("Person retrieved, name: {}", person.name);fetch_json_body fetches the response, requires a
successful status, and returns the owned value.
Pair fetch_json_ref with borrowed string fields such as Cow<'a, str>. Json<T>
borrows from the response buffer. Prefer Cow<'a, str> over &'a str. Cow uses an owned value
when an escaped JSON string cannot be borrowed.
// Define a type that can borrow string data.
#[derive(Serialize, Deserialize)]
struct Person<'a> {
id: u32,
#[serde(borrow)]
name: Cow<'a, str>,
}
let person = Person {
id: 1,
name: "Alice Johnson".into(),
};
// Send and receive zero-copy JSON.
let response: Response<Json<Person>> = client
.put("https://api.company.com/people")
.json(&person)
.fetch_json_ref::<Person>()
.await?;
// Read the response metadata.
let response = response.ensure_success()?;
// Extract the JSON wrapper.
let mut json_body: Json<Person> = response.into_body();
// Parse the JSON data using zero-copy deserialization.
// The parsed Person borrows string data from the underlying buffer.
let person: Person = json_body.read()?;
println!("Person retrieved, name: {}", person.name);Borrowed fields reduce heap allocations and copies. The parsed value cannot outlive the response buffer.
Each request passes through a sequence of RequestHandler implementations.
A handler can:
- Modify the request before the next handler receives it.
- Return a response without calling the next handler, for example to serve a cached response.
- Process the response from the next handler.
- Add logging, metrics, or other shared behavior.
The transport handler is the final RequestHandler. It performs I/O and returns a response.
fetch supplies the resilience, telemetry, routing, and logging layers above it. The transport
can use the bundled hyper implementation, custom runtime and I/O code, or another client such
as reqwest. See custom and custom::create_builder for a
custom transport.
The client supports three pipeline types:
The standard pipeline is the default. The outermost handler processes the request first and the response last.
See StandardRequestPipeline and
HttpClientBuilder::standard_pipeline.
A custom pipeline contains the handlers that its factory returns. See
HttpClientBuilder::custom_pipeline.
The minimal pipeline contains only Dispatch. Dispatch passes
each request to the transport.
// Create a client with only the dispatch handler.
let minimal_client = builder.minimal_pipeline().build();
// Wrap the client with custom processing.
let wrapped_client = MyHttpWrapper::new(minimal_client);Use this pipeline when another component supplies all middleware.
See RequestHandler to implement request or response processing.
Enable test-util to return fake responses without network I/O.
A fake client can:
- Return different HTTP responses.
- Exercise retry and error handling.
- Keep tests deterministic by avoiding network calls.
- Reproduce cases that are difficult to produce with a service.
HttpClient::new_fake creates a client from predefined responses or a
FakeHandler:
// Always return a 200 response.
let client = HttpClient::new_fake(StatusCode::OK);
// Return a sequence of responses without bodies.
let client = HttpClient::new_fake(vec![StatusCode::OK, StatusCode::INTERNAL_SERVER_ERROR]);
// Create a fake response.
let response = HttpResponseBuilder::new_fake()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.text("fake text")
.build()
.expect("always succeeds");
// Always return the same response.
let client = HttpClient::new_fake(response);
// Use a synchronous or asynchronous custom handler.
let fake_handler = FakeHandler::from_fn(|req| {
println!("Fake handler called for request, url: {}", req.uri());
HttpResponseBuilder::new_fake()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.text("fake text")
.build()
});
// Create a client that uses the custom handler.
let client = HttpClient::new_fake(fake_handler);fetch uses BytesView for request and response bodies. It provides:
- Memory pooling.
- Reduced copying.
- A single view over multiple chunks.
- Zero-copy operations when the data layout permits them.
- Compatibility with the
bytescrate.
BytesView implements Buf. Use BytesBuf, which implements BufMut, to build a
mutable body.
// Require a successful status and return the body as BytesView.
let mut body_bytes = client.get("https://example.com").fetch_bytes_body().await?;
// Use methods from bytes::Buf.
let length = body_bytes.remaining();
// Copy part of the body when required.
let mut buffer = vec![0; 10.min(length)];
if !body_bytes.is_empty() {
body_bytes.copy_to_slice(&mut buffer);
}This representation reduces memory use and copies for large bodies.
Reuse clients, parse repeated URIs once, and keep binary data in BytesView:
// Create one client and reuse it.
let client = HttpClient::new_tokio();
// Parse repeated URIs once.
let users_uri: Uri = "https://api.example.com/users".parse()?;
let items_uri: Uri = "https://api.example.com/items".parse()?;
// Keep binary data in BytesView.
let bytes = client.get(users_uri.clone()).fetch_bytes_body().await?;
process_binary_data(bytes);Creating an HttpClient sets up connection pools and security configuration. Create one client
and share it across tasks. Parse repeated Uri values once. Converting BytesView to
String allocates and copies data, so keep binary or large bodies in BytesView when possible.
fetch uses types from http and extensions from http_extensions. It re-exports common
types from both crates.
The seatbelt crate provides the built-in resilience layers.
Client handlers and seatbelt middleware implement Service, so the
pipeline can combine them without an adapter.
The available patterns include:
- Retries with configurable backoff.
- Timeouts.
- Circuit breakers.
The standard pipeline configures these patterns by default.
The client supports two TLS backends:
rustlsis the default. It usesrustlswith theaws-lc-rscrypto provider. Thetlsfeature enables it.native-tlsusesSChannelon Windows, Security Framework onmacOS, andOpenSSLon Linux. It is selected whennative-tlsis enabled withoutrustls.
With rustls, rustls-platform-verifier uses the
platform trust store. It:
- Validates certificates against the operating system root
CAs. - Validates host names and certificate expiration.
- Applies TLS security policies.
The default client applies the selected TLS backend:
use fetch::HttpClient;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = HttpClient::new_tokio();
// Send an HTTPS request.
let response = client.get("https://www.example.com").fetch_text().await?;
Ok(())
}Enable tls and tokio to use the default rustls transport:
fetch = { version = "*", features = ["tls", "tokio"] }Enable native-tls and tokio to use native TLS:
fetch = { version = "*", features = ["native-tls", "tokio"] }If both backends are enabled, use
TlsOptions::builder_rustls() or
TlsOptions::builder_native_tls() to select a backend for each client.
Automatic response decompression is off by default. Enable the required compression-*
features, or enable compression-all. Then select the formats through
DecompressionOptions:
let client = builder.decompression(&[DecompressionFormat::Gzip]).build();Requests list the formats in Accept-Encoding, with the most preferred format first. Before the
client returns a response that uses a selected format, it removes Content-Encoding and
Content-Length. It decodes the body lazily as the caller reads it. If the response uses a
format that was not selected, the client returns it without changes.
DecompressionFormat::ALL asks for everything the build can
decompress.
A malformed body fails when code reads it, not when the response arrives. This failure does not
trigger a retry. The client uses the http_compression defaults. The decompressed output limit
is 64 MiB. Each decompression stage accepts at most 1,024 concatenated streams. fetch does not
override these defaults.
DecompressionOptions sets limits for decompressed output and
compressed streams. Pass None to
max_output_len to remove the output limit.
The limits also apply to streamed bodies. Exceeding a limit returns the
compression_limit_exceeded error label.
response_body_options separately limits memory
used to buffer a body. Its default limit is 2 GiB.
For server-side compression and decompression, use the
http_compression crate directly.
Enable optional features in Cargo.toml:
[dependencies]
fetch = { version = "*", features = ["json", "tokio", "tls", "compression-gzip"] }-
tokioenables the Tokio runtime integration. Combine it withrustls,native-tls, or thetlsalias to provideHttpClient::builder_tokio,HttpClient::builder_tokio_with_options, and related APIs. The bundled transport requires a TLS feature even when the client permits HTTP. -
jsonenables JSON serialization and deserialization. It provides methods such asHttpRequestBuilder::jsonandHttpRequestBuilder::fetch_json. -
tlsenables HTTPS throughrustlsand theaws-lc-rscrypto provider. It is an alias forrustls. -
rustlsenables the default TLS backend withaws-lc-rs. -
native-tlsenablesSChannelon Windows, Security Framework onmacOS, andOpenSSLon Linux. If both TLS backends are enabled,rustlsis the default. UseTlsOptions::builder_native_tls()to select native TLS. -
test-utilprovides fake responses and client behavior for tests without network I/O.
The compression-* features select formats for
automatic response decompression. They are off by default.
A build without them links no compression implementation. Enabling a feature makes its format
available. The client does not decompress a response until
DecompressionOptions::formats
selects it and the client applies the options with
decompression.
-
compression-gziplinksgzipcompression (RFC 1952) and addsDecompressionFormat::Gzip. The legacyx-gziptoken also selects it. -
compression-deflatelinksdeflatecompression and addsDecompressionFormat::Deflate. Despite the token, the HTTPdeflateformat is zlib-wrapped DEFLATE (RFC 1950), not raw DEFLATE (RFC 1951). -
compression-brotlilinks Brotli compression (br, RFC 7932) and addsDecompressionFormat::Brotli. -
compression-zstdlinks Zstandard compression (zstd, RFC 8878) and addsDecompressionFormat::Zstd. -
compression-allenables all four compression features. UseDecompressionFormat::ALLto request decompression with all of them.
Note: Enable
tokioandtlsfor HTTPS with the bundled transport. Enablejsonfor JSON APIs.
This crate was developed as part of The Oxidizer Project. Browse this crate's source code.
