How do I build HTTP types with the http crate in Rust?

Walkthrough

The http crate provides core HTTP types that form the foundation for many Rust web libraries like hyper, reqwest, axum, and others. It defines strongly-typed representations of HTTP requests, responses, headers, method, status codes, and URIs. The crate is designed to be a shared foundation that different HTTP libraries can build upon, ensuring interoperability. Understanding http is essential for working with web services in Rust at a lower level or building your own HTTP abstractions.

Key concepts:

  1. Request — represents an HTTP request with body type T
  2. Response — represents an HTTP response with body type T
  3. Method — HTTP methods (GET, POST, PUT, DELETE, etc.)
  4. StatusCode — HTTP status codes with semantic meaning
  5. HeaderMap — type-safe header storage

Code Example

# Cargo.toml
[dependencies]
http = "1.0"
use http::{Request, Response, Method, StatusCode};
 
fn main() {
    let request = Request::builder()
        .method(Method::GET)
        .uri("https://example.com/api/users")
        .header("Content-Type", "application/json")
        .body(()).unwrap();
    
    println!("Request: {} {}", request.method(), request.uri());
}

Building Requests

use http::{Request, Method};
 
fn main() {
    // Simple request with builder
    let request = Request::builder()
        .method(Method::GET)
        .uri("/users/123")
        .body(())
        .unwrap();
    
    println!("Method: {}", request.method());
    println!("URI: {}", request.uri());
    
    // POST request with body
    let post_request = Request::builder()
        .method(Method::POST)
        .uri("/api/users")
        .header("Content-Type", "application/json")
        .body(r#"{"name":"Alice"}"#.to_string())
        .unwrap();
    
    println!("Body: {}", post_request.body());
}

Request Parts and Body

use http::Request;
 
fn main() {
    let request = Request::builder()
        .method("GET")
        .uri("https://example.com/search?q=rust")
        .header("Accept", "application/json")
        .header("User-Agent", "MyApp/1.0")
        .body("request body".to_string())
        .unwrap();
    
    // Decompose into parts and body
    let (parts, body) = request.into_parts();
    
    println!("Method: {:?}", parts.method);
    println!("URI: {:?}", parts.uri);
    println!("Version: {:?}", parts.version);
    println!("Headers: {:?}", parts.headers);
    println!("Body: {}", body);
}

Building Responses

use http::{Response, StatusCode};
 
fn main() {
    // Simple response
    let response = Response::builder()
        .status(StatusCode::OK)
        .header("Content-Type", "text/plain")
        .body("Hello, World!")
        .unwrap();
    
    println!("Status: {}", response.status());
    println!("Body: {:?}", response.body());
    
    // Response with different status
    let not_found = Response::builder()
        .status(StatusCode::NOT_FOUND)
        .header("Content-Type", "application/json")
        .body(r#"{"error":"Not found"}"#)
        .unwrap();
    
    println!("Status: {}", not_found.status());
}

HTTP Methods

use http::Method;
 
fn main() {
    // Common methods
    let get = Method::GET;
    let post = Method::POST;
    let put = Method::PUT;
    let delete = Method::DELETE;
    let patch = Method::PATCH;
    let head = Method::HEAD;
    let options = Method::OPTIONS;
    let connect = Method::CONNECT;
    let trace = Method::TRACE;
    
    println!("GET: {}", get);
    println!("POST: {}", post);
    
    // Custom method
    let custom = Method::from_bytes(b"CUSTOM").unwrap();
    println!("Custom: {}", custom);
    
    // Compare methods
    if get == Method::GET {
        println!("This is a GET request");
    }
    
    // Method properties
    println!("GET is safe: {}", get.is_safe());
    println!("POST is safe: {}", post.is_safe());
    println!("GET is idempotent: {}", get.is_idempotent());
}

Status Codes

use http::StatusCode;
 
fn main() {
    // Common status codes
    let ok = StatusCode::OK;
    let created = StatusCode::CREATED;
    let bad_request = StatusCode::BAD_REQUEST;
    let not_found = StatusCode::NOT_FOUND;
    let server_error = StatusCode::INTERNAL_SERVER_ERROR;
    
    println!("OK: {} - {}", ok.as_u16(), ok.canonical_reason().unwrap());
    
    // Status code categories
    for code in [200, 201, 301, 400, 404, 500] {
        let status = StatusCode::from_u16(code).unwrap();
        
        let category = if status.is_informational() {
            "Informational"
        } else if status.is_success() {
            "Success"
        } else if status.is_redirection() {
            "Redirection"
        } else if status.is_client_error() {
            "Client Error"
        } else if status.is_server_error() {
            "Server Error"
        } else {
            "Unknown"
        };
        
        println!("{}: {} - {:?}", code, category, status.canonical_reason());
    }
}

Working with Headers

use http::{HeaderMap, HeaderName, HeaderValue};
 
fn main() {
    let mut headers = HeaderMap::new();
    
    // Insert headers
    headers.insert("content-type", "application/json".parse().unwrap());
    headers.insert("authorization", "Bearer token123".parse().unwrap());
    
    // Append header (allows multiple values)
    headers.append("set-cookie", "session=abc".parse().unwrap());
    headers.append("set-cookie", "theme=dark".parse().unwrap());
    
    // Get single header
    if let Some(content_type) = headers.get("content-type") {
        println!("Content-Type: {:?}", content_type);
    }
    
    // Get all values for a header
    for cookie in headers.get_all("set-cookie").iter() {
        println!("Cookie: {:?}", cookie);
    }
    
    // Check existence
    println!("Has authorization: {}", headers.contains_key("authorization"));
    
    // Remove header
    headers.remove("authorization");
    
    // Iterate all headers
    for (name, value) in &headers {
        println!("{}: {:?}", name, value);
    }
}

Typed Header Names

use http::{header, HeaderMap};
 
fn main() {
    let mut headers = HeaderMap::new();
    
    // Use standard header names
    headers.insert(header::CONTENT_TYPE, "text/html".parse().unwrap());
    headers.insert(header::ACCEPT, "application/json".parse().unwrap());
    headers.insert(header::USER_AGENT, "MyApp/1.0".parse().unwrap());
    headers.insert(header::HOST, "example.com".parse().unwrap());
    headers.insert(header::AUTHORIZATION, "Bearer token".parse().unwrap());
    
    // Access using typed names
    if let Some(ct) = headers.get(header::CONTENT_TYPE) {
        println!("Content-Type: {:?}", ct);
    }
    
    // Common headers available in header module
    let _headers: &[&str] = &[
        "accept",
        "accept-encoding",
        "accept-language",
        "cache-control",
        "content-length",
        "content-type",
        "cookie",
        "date",
        "etag",
        "host",
        "if-modified-since",
        "location",
        "server",
        "set-cookie",
        "user-agent",
    ];
}

URI Parsing

use http::Uri;
 
fn main() {
    let uri: Uri = "https://example.com:8080/api/users?page=1&limit=10#section".parse().unwrap();
    
    // URI components
    println!("Scheme: {:?}", uri.scheme());
    println!("Authority: {:?}", uri.authority());
    println!("Path: {:?}", uri.path());
    println!("Query: {:?}", uri.query());
    println!("Fragment: {:?}", uri.fragment());
    
    // Build URI from parts
    let uri = Uri::builder()
        .scheme("https")
        .authority("api.example.com")
        .path_and_query("/v1/users?active=true")
        .build()
        .unwrap();
    
    println!("Built URI: {}", uri);
}

Authority and Path

use http::uri::{Authority, PathAndQuery, Uri};
 
fn main() {
    let uri: Uri = "https://user:pass@example.com:443/api/v1/users?limit=5".parse().unwrap();
    
    if let Some(auth) = uri.authority() {
        println!("Host: {:?}", auth.host());
        println!("Port: {:?}", auth.port());
        println!("Full authority: {}", auth);
    }
    
    if let Some(path) = uri.path_and_query() {
        println!("Path: {:?}", path.path());
        println!("Query: {:?}", path.query());
    }
    
    // Build authority
    let authority: Authority = "api.example.com:8080".parse().unwrap();
    println!("Authority: {}", authority);
    
    // Build path and query
    let pq: PathAndQuery = "/search?q=rust&sort=recent".parse().unwrap();
    println!("Path+Query: {}", pq);
}

Request Builder Patterns

use http::{Request, Method, HeaderMap};
 
fn main() {
    // Method 1: Builder pattern
    let request1 = Request::builder()
        .method(Method::POST)
        .uri("/api/users")
        .header("Content-Type", "application/json")
        .body(r#"{"name":"Alice"}"
#.to_string())
        .unwrap();
    
    // Method 2: Create from parts
    let mut builder = Request::builder()
        .method(Method::GET)
        .uri("/api/users");
    
    builder = builder.header("Accept", "application/json");
    builder = builder.header("User-Agent", "MyApp");
    
    let request2 = builder.body(()).unwrap();
    
    // Method 3: Modify existing request
    let mut request = Request::new("body");
    *request.method_mut() = Method::PUT;
    *request.uri_mut() = "/update".parse().unwrap();
    request.headers_mut().insert("X-Custom", "value".parse().unwrap());
    
    println!("Request: {} {}", request.method(), request.uri());
}

Response Builder Patterns

use http::{Response, StatusCode, header};
 
fn main() {
    // JSON response
    let json_response = Response::builder()
        .status(StatusCode::OK)
        .header(header::CONTENT_TYPE, "application/json")
        .body(r#"{"status":"ok"}"
#)
        .unwrap();
    
    // Error response
    let error_response = Response::builder()
        .status(StatusCode::BAD_REQUEST)
        .header(header::CONTENT_TYPE, "application/json")
        .body(r#"{"error":"Invalid input"}"
#)
        .unwrap();
    
    // Redirect response
    let redirect = Response::builder()
        .status(StatusCode::FOUND)
        .header(header::LOCATION, "/new-location")
        .body(())
        .unwrap();
    
    // Create response and modify
    let mut response = Response::new("body");
    *response.status_mut() = StatusCode::CREATED;
    response.headers_mut().insert(
        header::CONTENT_TYPE,
        "text/plain".parse().unwrap(),
    );
}

Extensions for Request/Response State

use http::{Request, Response, Extensions};
use std::any::Any;
 
#[derive(Debug, Clone)]
struct RequestContext {
    request_id: String,
    user_id: Option<String>,
}
 
fn main() {
    let mut request = Request::new(());
    
    // Add typed extension
    request.extensions_mut().insert(RequestContext {
        request_id: "req-123".to_string(),
        user_id: Some("user-456".to_string()),
    });
    
    // Retrieve extension
    if let Some(ctx) = request.extensions().get::<RequestContext>() {
        println!("Request ID: {}", ctx.request_id);
    }
    
    // Extensions are type-indexed
    request.extensions_mut().insert(42i32);
    request.extensions_mut().insert("hello".to_string());
    
    println!("Int: {:?}", request.extensions().get::<i32>());
    println!("String: {:?}", request.extensions().get::<String>());
}

Version

use http::{Version, Request};
 
fn main() {
    let mut request = Request::new(());
    
    // Set HTTP version
    *request.version_mut() = Version::HTTP_11;
    
    println!("Version: {:?}", request.version());
    
    // Available versions
    println!("HTTP/0.9: {:?}", Version::HTTP_09);
    println!("HTTP/1.0: {:?}", Version::HTTP_10);
    println!("HTTP/1.1: {:?}", Version::HTTP_11);
    println!("HTTP/2.0: {:?}", Version::HTTP_2);
    println!("HTTP/3.0: {:?}", Version::HTTP_3);
}

Converting Between Request and Response

use http::{Request, Response, StatusCode};
 
fn handle_request(req: Request<String>) -> Response<String> {
    println!("Handling {} {}", req.method(), req.uri());
    
    Response::builder()
        .status(StatusCode::OK)
        .header("Content-Type", "application/json")
        .body(format!("{{\"path\": \"{}\"}}", req.uri().path()))
        .unwrap()
}
 
fn main() {
    let request = Request::builder()
        .method("GET")
        .uri("/api/users")
        .body(String::new())
        .unwrap();
    
    let response = handle_request(request);
    println!("Response: {} - {}", response.status(), response.body());
}

Error Handling

use http::{Request, Method, StatusCode};
 
fn main() {
    // Invalid method
    let result = "INVALID METHOD".parse::<Method>();
    match result {
        Ok(method) => println!("Method: {}", method),
        Err(e) => println!("Invalid method: {}", e),
    }
    
    // Invalid status code
    let result = StatusCode::from_u16(999);
    match result {
        Ok(status) => println!("Status: {}", status),
        Err(e) => println!("Invalid status: {}", e),
    }
    
    // Invalid URI
    let result = "not a valid uri".parse::<http::Uri>();
    match result {
        Ok(uri) => println!("URI: {}", uri),
        Err(e) => println!("Invalid URI: {}", e),
    }
}

Generic Body Types

use http::{Request, Response};
 
// Different body types
fn process_requests() {
    // Empty body
    let empty: Request<()> = Request::builder()
        .uri("/health")
        .body(())
        .unwrap();
    
    // String body
    let string: Request<String> = Request::builder()
        .uri("/api")
        .body("text body".to_string())
        .unwrap();
    
    // Bytes body
    let bytes: Request<Vec<u8>> = Request::builder()
        .uri("/upload")
        .body(vec![0, 1, 2, 3, 4])
        .unwrap();
    
    // Custom body type
    #[derive(Debug)]
    struct JsonBody(serde_json::Value);
    
    let json = Request::builder()
        .uri("/api/json")
        .body(JsonBody(serde_json::json!({"key": "value"})))
        .unwrap();
}
 
fn main() {
    process_requests();
}

Middleware Pattern

use http::{Request, Response, header};
 
fn logging_middleware<T>(request: &Request<T>) {
    println!("[{}] {}", request.method(), request.uri());
    for (name, value) in request.headers() {
        println!("  {}: {:?}", name, value);
    }
}
 
fn auth_middleware<T>(request: &Request<T>) -> Result<(), String> {
    match request.headers().get(header::AUTHORIZATION) {
        Some(auth) => {
            println!("Auth header present: {:?}", auth);
            Ok(())
        }
        None => Err("Missing authorization header".to_string()),
    }
}
 
fn add_response_headers<B>(response: &mut Response<B>) {
    response.headers_mut().insert(
        "X-Request-Id",
        "req-123".parse().unwrap(),
    );
    response.headers_mut().insert(
        "X-Response-Time",
        "42ms".parse().unwrap(),
    );
}
 
fn main() {
    let request = Request::builder()
        .method("GET")
        .uri("/api/users")
        .header("Authorization", "Bearer token")
        .body(())
        .unwrap();
    
    logging_middleware(&request);
    
    if auth_middleware(&request).is_ok() {
        let mut response = Response::new("OK");
        add_response_headers(&mut response);
        println!("Response headers: {:?}", response.headers());
    }
}

Real-World Example: API Client

use http::{Request, Response, Method, StatusCode, header};
use std::collections::HashMap;
 
struct ApiClient {
    base_url: String,
    default_headers: HashMap<String, String>,
}
 
impl ApiClient {
    fn new(base_url: &str) -> Self {
        Self {
            base_url: base_url.to_string(),
            default_headers: HashMap::new(),
        }
    }
    
    fn with_header(mut self, key: &str, value: &str) -> Self {
        self.default_headers.insert(key.to_string(), value.to_string());
        self
    }
    
    fn build_request(&self, method: Method, path: &str, body: Option<&str>) -> Request<String> {
        let uri = format!("{}{}", self.base_url, path);
        
        let mut builder = Request::builder()
            .method(method)
            .uri(&uri);
        
        // Add default headers
        for (key, value) in &self.default_headers {
            builder = builder.header(key, value);
        }
        
        builder.body(body.unwrap_or("").to_string()).unwrap()
    }
    
    fn get(&self, path: &str) -> Request<String> {
        self.build_request(Method::GET, path, None)
    }
    
    fn post(&self, path: &str, body: &str) -> Request<String> {
        self.build_request(Method::POST, path, Some(body))
    }
}
 
fn main() {
    let client = ApiClient::new("https://api.example.com")
        .with_header("Accept", "application/json")
        .with_header("User-Agent", "MyApp/1.0");
    
    let get_req = client.get("/users");
    println!("GET {}", get_req.uri());
    
    let post_req = client.post("/users", r#"{"name":"Alice"}"
#);
    println!("POST {} with body: {}", post_req.uri(), post_req.body());
}

Real-World Example: HTTP Router

use http::{Request, Response, Method, StatusCode};
 
type Handler = fn(&Request<String>) -> Response<String>;
 
struct Router {
    routes: Vec<(Method, String, Handler)>,
}
 
impl Router {
    fn new() -> Self {
        Self { routes: Vec::new() }
    }
    
    fn route(mut self, method: Method, path: &str, handler: Handler) -> Self {
        self.routes.push((method, path.to_string(), handler));
        self
    }
    
    fn handle(&self, request: &Request<String>) -> Response<String> {
        for (method, path, handler) in &self.routes {
            if request.method() == method && request.uri().path() == path {
                return handler(request);
            }
        }
        
        Response::builder()
            .status(StatusCode::NOT_FOUND)
            .body("Not Found".to_string())
            .unwrap()
    }
}
 
fn get_users(_req: &Request<String>) -> Response<String> {
    Response::builder()
        .status(StatusCode::OK)
        .body(r#"[{"id":1,"name":"Alice"}]"#.to_string())
        .unwrap()
}
 
fn create_user(req: &Request<String>) -> Response<String> {
    println!("Creating user with: {}", req.body());
    Response::builder()
        .status(StatusCode::CREATED)
        .body(r#"{"id":2,"name":"Bob"}"
#.to_string())
        .unwrap()
}
 
fn main() {
    let router = Router::new()
        .route(Method::GET, "/users", get_users)
        .route(Method::POST, "/users", create_user);
    
    let request = Request::builder()
        .method(Method::GET)
        .uri("/users")
        .body(String::new())
        .unwrap();
    
    let response = router.handle(&request);
    println!("Response: {} - {}", response.status(), response.body());
}

Summary

  • Request<T> and Response<T> are generic over body type T
  • Use Request::builder() and Response::builder() for fluent construction
  • Method provides type-safe HTTP methods with GET, POST, etc.
  • StatusCode offers all HTTP status codes with semantic methods
  • HeaderMap stores headers with efficient lookup and multiple values
  • Use header module for standard header names like CONTENT_TYPE
  • Uri parses URIs into components: scheme, authority, path, query
  • Extensions allows attaching typed data to requests/responses
  • into_parts() and from_parts() decompose and reconstruct messages
  • The crate is designed for interoperability between HTTP libraries
  • Perfect for: building HTTP clients/servers, middleware, API clients, routing