How to use http crate in Rust HTTP types

The `http` crate provides the foundational, platform-agnostic types (like `Request`, `Response`, `HeaderMap`, and `Method`) used by the Rust ecosystem, but it does not include an HTTP client or server implementation itself.

When libraries need a common language

You are writing a logging middleware for a web server. You want to capture the incoming request, print the headers, and pass the request along to the handler. You reach for your server library, but you realize the request type is buried inside the library's namespace. You want to write a unit test that constructs a request without spinning up a server, but the library's types are hard to build manually. You also want to share a request object between a custom retry mechanism and the actual sender, but the types don't match.

The ecosystem solved this with the http crate. It defines the standard data structures for HTTP messages. Every major client and server library uses these types. reqwest uses them. hyper uses them. actix-web uses them. The http crate is the lingua franca. It gives you a shared vocabulary so you can pass data between libraries, write tests, and build middleware without locking yourself into one implementation.

The standard envelope

The http crate provides Request, Response, HeaderMap, Method, Uri, StatusCode, and Version. It does not send or receive data. It does not open sockets. It does not resolve DNS. It does not implement HTTP/2 framing. It is purely the data model.

Think of the http crate as the standard envelope format. It defines what an envelope looks like, where the stamp goes, how to write the address, and what counts as valid ink. It does not deliver the mail. For delivery, you need a postal service. In Rust, reqwest is the postal service for clients. hyper is the postal service for servers. The http crate gives you the envelope; the other crates handle the logistics.

This separation is deliberate. It keeps the data structures stable and lightweight. It allows libraries to evolve their networking code without breaking the types you rely on. It also means you can construct and manipulate HTTP messages in memory without touching the network at all.

Building a request

The standard way to create an http::Request is the builder pattern. You start with Request::builder(), chain methods to set the parts, and call body() to produce the final request. The builder returns a Result because you can forget required fields like the URI.

use http::{Request, Response, Method, StatusCode, HeaderMap, HeaderValue};

fn build_example() -> Result<(), http::Error> {
    // Builder accumulates state. body() consumes the builder and returns Result.
    let req = Request::builder()
        .method(Method::POST)
        .uri("https://api.example.com/upload")
        .header("Content-Type", "application/json")
        .body(vec![0u8; 1024])?;

    // Headers are stored in a HeaderMap. Access returns Option<&HeaderValue>.
    if let Some(ct) = req.headers().get("Content-Type") {
        println!("Type: {}", ct.to_str().unwrap());
    }

    // Response builder follows the same pattern.
    let res = Response::builder()
        .status(StatusCode::OK)
        .body("Done")?;

    Ok(())
}

The builder enforces correctness. If you omit .uri(), the body() call returns an error. The builder also validates headers as you add them. Invalid header values are rejected immediately. This catches mistakes at construction time rather than letting them propagate to the network layer.

The body method is generic. The type parameter B in Request<B> can be anything. In the example, the body is Vec<u8>. It could be a String, a custom struct, or a stream type. The http crate does not care what the body contains. It just holds it. This flexibility is why the crate works everywhere. Libraries can choose the body type that fits their needs.

Don't try to reuse the builder. The body() method consumes it. If you need multiple requests, create multiple builders.

Inside the builder

The builder pattern in http is designed for safety and ergonomics. Methods like method(), uri(), and header() return the builder itself, allowing chaining. The body() method is the terminator. It takes ownership of the body value and the builder, validates the state, and returns Result<Request<B>, Error>.

The error type is http::Error. It covers missing fields, invalid headers, and invalid URIs. The error message tells you exactly what went wrong. If you forget the URI, you get a clear message. If you pass a header value with invalid bytes, you get a validation error.

The Method type is not just a string. It is a structured type with known variants like GET, POST, PUT, DELETE. You can also create custom methods, but the standard ones are preferred. The StatusCode type works similarly. It provides constants like StatusCode::OK and StatusCode::NOT_FOUND, plus methods to convert to and from integers.

The Uri type represents the request URI. It is immutable once constructed. You cannot modify a Uri in place. If you need to change the path or query, you build a new Uri. This immutability allows URIs to be shared safely across threads and borrowed without cloning the underlying string.

Treat the builder as a one-time factory. Build, validate, and move on.

HeaderMap and the header rules

Headers are stored in a HeaderMap. This is not a HashMap<String, String>. It is a specialized structure optimized for HTTP. Keys are HeaderName types. Values are HeaderValue types. The map is case-insensitive for keys. Content-Type, content-type, and CONTENT-TYPE all refer to the same header.

use http::{HeaderMap, HeaderValue, header::CONTENT_TYPE};

fn header_rules() -> Result<(), http::Error> {
    let mut headers = HeaderMap::new();

    // insert replaces existing values. Returns the old value if present.
    headers.insert(CONTENT_TYPE, HeaderValue::from_static("text/plain"));

    // append adds a second value. Some headers allow multiple values.
    headers.append("Accept", HeaderValue::from_static("text/html"));
    headers.append("Accept", HeaderValue::from_static("application/json"));

    // get returns the first value. get_all returns an iterator over all values.
    let accepts = headers.get_all("Accept");
    for accept in accepts {
        println!("Accept: {}", accept.to_str().unwrap());
    }

    Ok(())
}

The distinction between insert and append matters. insert replaces any existing value for that key. append adds a new value, allowing multiple entries for the same header. This is essential for headers like Set-Cookie, Accept, and Vary, where multiple values are valid and meaningful.

HeaderValue validates content. Not every byte sequence is a valid header value. The HTTP spec restricts headers to ASCII characters with some exceptions. HeaderValue::from_bytes checks these rules and returns an error if the value is invalid. This prevents malformed headers from leaking into your application.

Convention aside: use the constants from http::header like CONTENT_TYPE instead of string literals. The constants are HeaderName types, so they are validated at compile time and avoid typos. Writing "Content-Type" works, but CONTENT_TYPE is safer and faster.

Iteration order in HeaderMap is not guaranteed. Do not rely on the order of headers. If order matters, collect into a vector and sort.

The generic body

The body type parameter B in Request<B> and Response<B> is the source of most confusion for newcomers. The http crate does not define what a body is. It leaves that to the consumer. This design allows the crate to support bytes, strings, streams, and future types without imposing a single model.

In a simple client, the body might be Vec<u8> for binary data or String for text. In a streaming server, the body might be a type that implements a stream trait, yielding chunks of data over time. The http crate does not care. It just holds the body.

This generic approach means you cannot mix body types freely. A Request<Vec<u8>> is not the same as a Request<String>. If a function expects Request<Vec<u8>>, you cannot pass Request<String> even if the string can be converted to bytes. You must convert the body type explicitly.

The compiler will reject mismatched types with E0308 (mismatched types). This error often appears when you try to pass a request with one body type to a function expecting another. The fix is to convert the body before constructing the request, or to use a generic function that accepts any body type.

Pick the body type that matches your data flow. If you have bytes, use Vec<u8>. If you have a string, use String. If you are streaming, use a stream type. Do not fight the generic.

Extensions: the shared backpack

Request and Response have an Extensions map. This is a type-safe key-value store that holds arbitrary data. Libraries use extensions to attach metadata to requests and responses without breaking the API.

For example, a server library might store the original connection information in extensions. A client library might store the retry count. Middleware might store authentication results. Extensions allow the ecosystem to share data without adding fields to the core types.

use http::Request;

struct AuthInfo {
    user_id: u64,
    roles: Vec<String>,
}

fn use_extensions() {
    let mut req = Request::builder()
        .uri("https://api.example.com/secure")
        .body(())
        .unwrap();

    // Insert custom data into extensions.
    req.extensions_mut().insert(AuthInfo {
        user_id: 42,
        roles: vec!["admin".to_string()],
    });

    // Retrieve data from extensions.
    if let Some(auth) = req.extensions().get::<AuthInfo>() {
        println!("User: {}", auth.user_id);
    }
}

Extensions are stored as dyn Any. You must specify the type when inserting and retrieving. This provides type safety. If you try to get a type that was not inserted, you get None.

Treat extensions as the ecosystem's shared backpack. Check for existing keys before adding your own. Use well-known types when possible to avoid collisions.

Real-world interop

The power of the http crate shines when you integrate with libraries. reqwest and hyper both use http types internally. You can convert between library types and http types to inspect or modify messages.

use http::Request;
use reqwest::Client;

async fn inspect_with_reqwest() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::new();

    // Build a reqwest request.
    let reqwest_req = client.post("https://api.example.com")
        .json(&serde_json::json!({"key": "value"}))
        .build()?;

    // Convert to http::Request to inspect.
    // try_into consumes the reqwest request and returns http::Request<Vec<u8>>.
    let http_req: Request<Vec<u8>> = reqwest_req.try_into()?;

    println!("Method: {}", http_req.method());
    println!("URI: {}", http_req.uri());

    // You cannot send http_req directly.
    // reqwest owns the transport logic. http::Request is just data.
    Ok(())
}

The try_into() call converts the reqwest::Request into an http::Request<Vec<u8>>. This consumes the reqwest request. You cannot send the http request directly. You need the reqwest client to handle the network. This pattern is useful for logging, validation, or middleware that needs to peek at the raw message.

Some libraries support converting back. reqwest allows you to build a request from an http::Request using Client::from_request(). This lets you construct a standard request and send it with reqwest.

The http crate gives you the vocabulary. You still need a mouth to speak.

Pitfalls and compiler errors

The http crate is simple, but it has traps.

The crate does not send data. You cannot call req.send(). If you try, the compiler rejects you with E0599 (no function named send found). You must use a client or server library to transmit the request.

The Uri type is immutable. You cannot modify a URI after construction. If you need to change the path, build a new Uri. Attempting to mutate a URI results in a borrow checker error.

HeaderValue validation is strict. HeaderValue::from_bytes fails on invalid bytes. If you construct a header value from untrusted input, handle the error. Ignoring the error with unwrap() can crash your application.

Body types are rigid. Request<Vec<u8>> is not Request<String>. The compiler rejects mismatches with E0308. Convert body types explicitly before passing them to functions.

HeaderMap iteration order is undefined. Do not rely on the order of headers. If you need deterministic order, collect and sort.

Extensions require type matching. get::<T>() returns None if the type does not match. Check for None before using the value.

Don't fight the compiler here. The errors are protecting you from malformed HTTP and type mismatches.

Decision matrix

Use http when you need a standard data structure to pass between libraries or store HTTP data in memory. Use reqwest when you need to send requests over the network with a high-level API. Use hyper when you are building a server or need low-level control over the connection. Use url when you need to parse query strings or manipulate URL components, since http::Uri is minimal and immutable. Use httparse when you need to parse raw bytes from a socket without the overhead of full types.

The http crate is the foundation. Build on it, but reach for the right tool for the job.

Where to go next