How do I build HTTP servers with hyper in Rust?
Walkthrough
The hyper crate is a fast, correct, and low-level HTTP library for Rust. It's the foundation for popular web frameworks like axum, warp, and actix-web. Hyper provides both client and server functionality with full HTTP/1 and HTTP/2 support. It's built on Tokio for async I/O and offers fine-grained control over HTTP operations. While more verbose than higher-level frameworks, hyper gives you maximum flexibility and performance for building HTTP services.
Key concepts:
- Server — binds to an address and handles incoming connections
- Service trait — defines how to process requests into responses
- Body — streams of bytes for request/response bodies
- Request/Response — HTTP message types
- Routing — pattern matching on request method and path
Code Example
# Cargo.toml
[dependencies]
hyper = { version = "1.4", features = ["full"] }
tokio = { version = "1", features = ["full"] }
http-body-util = "0.1"
hyper-util = { version = "0.1", features = ["full"] }use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Request, Response, Body, Method, StatusCode};
use http_body_util::Full;
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
async fn hello(req: Request<hyper::body::Incoming>) -> Result<Response<Full<hyper::body::Bytes>>, hyper::Error> {
Ok(Response::new(Full::new(hyper::body::Bytes::from("Hello, World!"))))
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let addr = "127.0.0.1:3000";
let listener = TcpListener::bind(addr).await?;
println!("Listening on {}", addr);
loop {
let (stream, _) = listener.accept().await?;
let io = TokioIo::new(stream);
tokio::spawn(async move {
if let Err(err) = http1::Builder::new().serve_connection(io, service_fn(hello)).await {
eprintln!("Error serving connection: {:?}", err);
}
});
}
}Basic Server with Routing
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Method, Request, Response, StatusCode};
use http_body_util::Full;
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use std::convert::Infallible;
async fn handle_request(req: Request<hyper::body::Incoming>) -> Result<Response<Full<hyper::body::Bytes>>, Infallible> {
match (req.method(), req.uri().path()) {
(&Method::GET, "/") => {
Ok(Response::new(Full::new(hyper::body::Bytes::from("Welcome home!"))))
}
(&Method::GET, "/health") => {
Ok(Response::new(Full::new(hyper::body::Bytes::from("OK"))))
}
(&Method::POST, "/echo") => {
Ok(Response::new(Full::new(hyper::body::Bytes::from("Echo endpoint"))))
}
_ => {
let mut not_found = Response::new(Full::new(hyper::body::Bytes::from("Not Found")));
*not_found.status_mut() = StatusCode::NOT_FOUND;
Ok(not_found)
}
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let addr = "127.0.0.1:3000";
let listener = TcpListener::bind(addr).await?;
println!("Server running at http://{}", addr);
loop {
let (stream, _) = listener.accept().await?;
let io = TokioIo::new(stream);
tokio::spawn(async move {
if let Err(err) = http1::Builder::new()
.serve_connection(io, service_fn(handle_request))
.await
{
eprintln!("Error: {:?}", err);
}
});
}
}Reading Request Body
use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Method, Request, Response, StatusCode};
use http_body_util::{BodyExt, Full};
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use std::convert::Infallible;
async fn handle_request(req: Request<hyper::body::Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
match (req.method(), req.uri().path()) {
(&Method::POST, "/echo") => {
// Collect the entire body
let body_bytes = req.collect().await.unwrap().to_bytes();
// Echo it back
Ok(Response::new(Full::new(body_bytes)))
}
(&Method::POST, "/length") => {
let body_bytes = req.collect().await.unwrap().to_bytes();
let response = format!("Body length: {} bytes", body_bytes.len());
Ok(Response::new(Full::new(Bytes::from(response))))
}
_ => {
let mut response = Response::new(Full::new(Bytes::from("Not Found")));
*response.status_mut() = StatusCode::NOT_FOUND;
Ok(response)
}
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let listener = TcpListener::bind("127.0.0.1:3000").await?;
loop {
let (stream, _) = listener.accept().await?;
let io = TokioIo::new(stream);
tokio::spawn(async move {
let _ = http1::Builder::new()
.serve_connection(io, service_fn(handle_request))
.await;
});
}
}JSON Request and Response
use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Method, Request, Response, StatusCode, header};
use http_body_util::{BodyExt, Full};
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use serde::{Deserialize, Serialize};
use std::convert::Infallible;
#[derive(Deserialize)]
struct UserRequest {
name: String,
email: String,
}
#[derive(Serialize)]
struct UserResponse {
id: u64,
name: String,
email: String,
message: String,
}
async fn handle_request(req: Request<hyper::body::Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
match (req.method(), req.uri().path()) {
(&Method::POST, "/users") => {
// Read body
let body_bytes = req.collect().await.unwrap().to_bytes();
// Parse JSON
match serde_json::from_slice::<UserRequest>(&body_bytes) {
Ok(user) => {
// Create response
let response = UserResponse {
id: 1,
name: user.name,
email: user.email,
message: "User created successfully".to_string(),
};
let json = serde_json::to_string(&response).unwrap();
let mut res = Response::new(Full::new(Bytes::from(json)));
res.headers_mut().insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static("application/json"),
);
*res.status_mut() = StatusCode::CREATED;
Ok(res)
}
Err(e) => {
let mut res = Response::new(Full::new(Bytes::from(format!("Invalid JSON: {}", e))));
*res.status_mut() = StatusCode::BAD_REQUEST;
Ok(res)
}
}
}
_ => {
let mut res = Response::new(Full::new(Bytes::from("Not Found")));
*res.status_mut() = StatusCode::NOT_FOUND;
Ok(res)
}
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let listener = TcpListener::bind("127.0.0.1:3000").await?;
println!("Server running at http://127.0.0.1:3000");
loop {
let (stream, _) = listener.accept().await?;
let io = TokioIo::new(stream);
tokio::spawn(async move {
let _ = http1::Builder::new()
.serve_connection(io, service_fn(handle_request))
.await;
});
}
}Query Parameters
use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Method, Request, Response, Uri};
use http_body_util::Full;
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use std::collections::HashMap;
use std::convert::Infallible;
fn parse_query(uri: &Uri) -> HashMap<String, String> {
let mut params = HashMap::new();
if let Some(query) = uri.query() {
for pair in query.split('&') {
if let Some((key, value)) = pair.split_once('=') {
params.insert(
urlencoding::decode(key).unwrap().to_string(),
urlencoding::decode(value).unwrap().to_string(),
);
}
}
}
params
}
async fn handle_request(req: Request<hyper::body::Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
let params = parse_query(req.uri());
let name = params.get("name").map(|s| s.as_str()).unwrap_or("World");
let count = params.get("count").and_then(|s| s.parse::<u32>().ok()).unwrap_or(1);
let message = format!("Hello, {}! (x{})", name, count);
Ok(Response::new(Full::new(Bytes::from(message))))
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let listener = TcpListener::bind("127.0.0.1:3000").await?;
println!("Try: http://127.0.0.1:3000?name=Rust&count=5");
loop {
let (stream, _) = listener.accept().await?;
let io = TokioIo::new(stream);
tokio::spawn(async move {
let _ = http1::Builder::new()
.serve_connection(io, service_fn(handle_request))
.await;
});
}
}Path Parameters
use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Method, Request, Response, StatusCode};
use http_body_util::Full;
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use std::convert::Infallible;
// Simple path segment extraction
fn extract_path_segments(path: &str) -> Vec<&str> {
path.split('/').filter(|s| !s.is_empty()).collect()
}
async fn handle_request(req: Request<hyper::body::Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
let path = req.uri().path();
let segments = extract_path_segments(path);
match (req.method(), segments.as_slice()) {
(&Method::GET, ["users", id]) => {
let response = format!("User ID: {}", id);
Ok(Response::new(Full::new(Bytes::from(response))))
}
(&Method::GET, ["users", user_id, "posts", post_id]) => {
let response = format!("User {}'s Post {}", user_id, post_id);
Ok(Response::new(Full::new(Bytes::from(response))))
}
(&Method::GET, ["users"]) => {
Ok(Response::new(Full::new(Bytes::from("List all users"))))
}
_ => {
let mut res = Response::new(Full::new(Bytes::from("Not Found")));
*res.status_mut() = StatusCode::NOT_FOUND;
Ok(res)
}
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let listener = TcpListener::bind("127.0.0.1:3000").await?;
println!("Try:");
println!(" http://127.0.0.1:3000/users");
println!(" http://127.0.0.1:3000/users/42");
println!(" http://127.0.0.1:3000/users/42/posts/1");
loop {
let (stream, _) = listener.accept().await?;
let io = TokioIo::new(stream);
tokio::spawn(async move {
let _ = http1::Builder::new()
.serve_connection(io, service_fn(handle_request))
.await;
});
}
}Headers
use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{header, Method, Request, Response, StatusCode};
use http_body_util::Full;
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use std::convert::Infallible;
async fn handle_request(req: Request<hyper::body::Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
// Read request headers
let user_agent = req.headers()
.get(header::USER_AGENT)
.and_then(|v| v.to_str().ok())
.unwrap_or("Unknown");
let content_type = req.headers()
.get(header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.unwrap_or("Not specified");
let auth = req.headers()
.get("Authorization")
.and_then(|v| v.to_str().ok())
.unwrap_or("No auth");
// Build response with custom headers
let mut res = Response::new(Full::new(Bytes::from(format!(
"User-Agent: {}\nContent-Type: {}\nAuth: {}",
user_agent, content_type, auth
))));
// Set response headers
res.headers_mut().insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static("text/plain"),
);
res.headers_mut().insert(
header::CACHE_CONTROL,
header::HeaderValue::from_static("no-cache"),
);
res.headers_mut().insert(
"X-Custom-Header",
header::HeaderValue::from_static("Custom Value"),
);
Ok(res)
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let listener = TcpListener::bind("127.0.0.1:3000").await?;
loop {
let (stream, _) = listener.accept().await?;
let io = TokioIo::new(stream);
tokio::spawn(async move {
let _ = http1::Builder::new()
.serve_connection(io, service_fn(handle_request))
.await;
});
}
}Static File Server
use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{header, Method, Request, Response, StatusCode, Body};
use http_body_util::{BodyExt, Full, StreamBody};
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use tokio::fs::File;
use tokio::io::AsyncReadExt;
use std::path::PathBuf;
use std::convert::Infallible;
fn content_type(path: &str) -> &'static str {
match PathBuf::from(path).extension().and_then(|s| s.to_str()) {
Some("html") => "text/html",
Some("css") => "text/css",
Some("js") => "application/javascript",
Some("json") => "application/json",
Some("png") => "image/png",
Some("jpg") | Some("jpeg") => "image/jpeg",
Some("txt") => "text/plain",
_ => "application/octet-stream",
}
}
async fn serve_file(path: &str) -> Result<Response<Full<Bytes>>, Infallible> {
// Remove leading slash and prevent directory traversal
let safe_path = path.trim_start_matches('/');
if safe_path.contains("..") {
return Ok(Response::builder()
.status(StatusCode::FORBIDDEN)
.body(Full::new(Bytes::from("Forbidden")))
.unwrap());
}
let full_path = PathBuf::from("static").join(safe_path);
match File::open(&full_path).await {
Ok(mut file) => {
let mut contents = Vec::new();
if file.read_to_end(&mut contents).await.is_err() {
return Ok(Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Full::new(Bytes::from("Read error")))
.unwrap());
}
let ct = content_type(path);
Ok(Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, ct)
.body(Full::new(Bytes::from(contents)))
.unwrap())
}
Err(_) => {
Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Full::new(Bytes::from("Not Found")))
.unwrap())
}
}
}
async fn handle_request(req: Request<hyper::body::Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
let path = req.uri().path();
// Default to index.html
let file_path = if path == "/" { "/index.html" } else { path };
serve_file(file_path).await
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let listener = TcpListener::bind("127.0.0.1:3000").await?;
println!("Static file server running at http://127.0.0.1:3000");
loop {
let (stream, _) = listener.accept().await?;
let io = TokioIo::new(stream);
tokio::spawn(async move {
let _ = http1::Builder::new()
.serve_connection(io, service_fn(handle_request))
.await;
});
}
}Streaming Response
use hyper::body::{Bytes, Frame};
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Request, Response, header};
use http_body_util::StreamBody;
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use tokio_stream::wrappers::ReceiverStream;
use std::convert::Infallible;
use tokio::sync::mpsc;
use futures_util::StreamExt;
async fn handle_request(_req: Request<hyper::body::Incoming>) -> Result<Response<StreamBody<ReceiverStream<Result<Frame<Bytes>, Infallible>>>>, Infallible> {
let (tx, rx) = mpsc::channel::<Result<Frame<Bytes>, Infallible>>(10);
// Spawn task that sends chunks
tokio::spawn(async move {
for i in 0..5 {
let chunk = format!("Chunk {}\n", i);
tx.send(Ok(Frame::data(Bytes::from(chunk)))).await.unwrap();
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
}
});
let stream = ReceiverStream::new(rx);
let body = StreamBody::new(stream);
let mut response = Response::new(body);
response.headers_mut().insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static("text/plain"),
);
Ok(response)
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let listener = TcpListener::bind("127.0.0.1:3000").await?;
println!("Streaming server at http://127.0.0.1:3000");
loop {
let (stream, _) = listener.accept().await?;
let io = TokioIo::new(stream);
tokio::spawn(async move {
let _ = http1::Builder::new()
.serve_connection(io, service_fn(handle_request))
.await;
});
}
}Server-Sent Events (SSE)
use hyper::body::{Bytes, Frame};
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Request, Response, header};
use http_body_util::StreamBody;
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use tokio_stream::wrappers::ReceiverStream;
use std::convert::Infallible;
use tokio::sync::mpsc;
async fn handle_sse(_req: Request<hyper::body::Incoming>) -> Result<Response<StreamBody<ReceiverStream<Result<Frame<Bytes>, Infallible>>>>, Infallible> {
let (tx, rx) = mpsc::channel(10);
tokio::spawn(async move {
for i in 0..10 {
let event = format!("data: Message {}\n\n", i);
tx.send(Ok(Frame::data(Bytes::from(event)))).await.unwrap();
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
}
});
let stream = ReceiverStream::new(rx);
let body = StreamBody::new(stream);
let mut response = Response::new(body);
response.headers_mut().insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static("text/event-stream"),
);
response.headers_mut().insert(
header::CACHE_CONTROL,
header::HeaderValue::from_static("no-cache"),
);
response.headers_mut().insert(
header::CONNECTION,
header::HeaderValue::from_static("keep-alive"),
);
Ok(response)
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let listener = TcpListener::bind("127.0.0.1:3000").await?;
println!("SSE endpoint at http://127.0.0.1:3000/events");
loop {
let (stream, _) = listener.accept().await?;
let io = TokioIo::new(stream);
tokio::spawn(async move {
let _ = http1::Builder::new()
.serve_connection(io, service_fn(handle_sse))
.await;
});
}
}Middleware Pattern
use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Method, Request, Response, StatusCode, header};
use http_body_util::Full;
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use std::convert::Infallible;
use std::time::Instant;
// Logging middleware
async fn log_request(req: &Request<hyper::body::Incoming>) {
println!("[{}] {} {}",
chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
req.method(),
req.uri().path()
);
}
// Auth middleware
fn check_auth(req: &Request<hyper::body::Incoming>) -> bool {
req.headers()
.get("Authorization")
.and_then(|v| v.to_str().ok())
.map(|s| s == "Bearer secret-token")
.unwrap_or(false)
}
// Timing middleware
async fn with_timing<F, Fut>(req: Request<hyper::body::Incoming>, handler: F) -> Response<Full<Bytes>>
where
F: FnOnce(Request<hyper::body::Incoming>) -> Fut,
Fut: std::future::Future<Output = Result<Response<Full<Bytes>>, Infallible>>,
{
let start = Instant::now();
let method = req.method().clone();
let path = req.uri().path().to_string();
let response = handler(req).await;
let elapsed = start.elapsed();
println!("[TIMING] {} {} - {:?}", method, path, elapsed);
response.unwrap()
}
async fn handle_request(req: Request<hyper::body::Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
log_request(&req).await;
// Check authentication for protected routes
if req.uri().path().starts_with("/api/") {
if !check_auth(&req) {
let mut res = Response::new(Full::new(Bytes::from("Unauthorized")));
*res.status_mut() = StatusCode::UNAUTHORIZED;
return Ok(res);
}
}
match (req.method(), req.uri().path()) {
(&Method::GET, "/") => {
Ok(Response::new(Full::new(Bytes::from("Public endpoint"))))
}
(&Method::GET, "/api/data") => {
Ok(Response::new(Full::new(Bytes::from("Protected data"))))
}
_ => {
let mut res = Response::new(Full::new(Bytes::from("Not Found")));
*res.status_mut() = StatusCode::NOT_FOUND;
Ok(res)
}
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let listener = TcpListener::bind("127.0.0.1:3000").await?;
println!("Try:");
println!(" curl http://127.0.0.1:3000/");
println!(" curl http://127.0.0.1:3000/api/data");
println!(" curl -H 'Authorization: Bearer secret-token' http://127.0.0.1:3000/api/data");
loop {
let (stream, _) = listener.accept().await?;
let io = TokioIo::new(stream);
tokio::spawn(async move {
let _ = http1::Builder::new()
.serve_connection(io, service_fn(handle_request))
.await;
});
}
}Shared State
use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Method, Request, Response, StatusCode};
use http_body_util::Full;
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::convert::Infallible;
struct AppState {
request_count: AtomicU64,
start_time: std::time::Instant,
}
impl AppState {
fn new() -> Self {
Self {
request_count: AtomicU64::new(0),
start_time: std::time::Instant::now(),
}
}
fn increment_requests(&self) -> u64 {
self.request_count.fetch_add(1, Ordering::Relaxed)
}
fn get_request_count(&self) -> u64 {
self.request_count.load(Ordering::Relaxed)
}
fn uptime(&self) -> std::time::Duration {
self.start_time.elapsed()
}
}
async fn handle_request(
req: Request<hyper::body::Incoming>,
state: Arc<AppState>,
) -> Result<Response<Full<Bytes>>, Infallible> {
let count = state.increment_requests();
match (req.method(), req.uri().path()) {
(&Method::GET, "/stats") => {
let stats = format!(
"Requests: {}\nUptime: {:?}",
state.get_request_count(),
state.uptime()
);
Ok(Response::new(Full::new(Bytes::from(stats))))
}
(&Method::GET, "/") => {
Ok(Response::new(Full::new(Bytes::from(format!(
"Hello! Request #{}", count + 1
)))))
}
_ => {
let mut res = Response::new(Full::new(Bytes::from("Not Found")));
*res.status_mut() = StatusCode::NOT_FOUND;
Ok(res)
}
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let listener = TcpListener::bind("127.0.0.1:3000").await?;
let state = Arc::new(AppState::new());
println!("Server running at http://127.0.0.1:3000");
println!("Stats at http://127.0.0.1:3000/stats");
loop {
let (stream, _) = listener.accept().await?;
let io = TokioIo::new(stream);
let state = Arc::clone(&state);
tokio::spawn(async move {
let _ = http1::Builder::new()
.serve_connection(io, service_fn(move |req| {
let state = Arc::clone(&state);
async move { handle_request(req, state).await }
}))
.await;
});
}
}Graceful Shutdown
use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Request, Response};
use http_body_util::Full;
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use tokio::signal;
use std::convert::Infallible;
async fn handle_request(_req: Request<hyper::body::Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
Ok(Response::new(Full::new(Bytes::from("Hello!"))))
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let listener = TcpListener::bind("127.0.0.1:3000").await?;
println!("Server running. Press Ctrl+C to shut down.");
tokio::spawn(async {
signal::ctrl_c().await.expect("Failed to listen for Ctrl+C");
println!("\nShutting down...");
std::process::exit(0);
});
loop {
let (stream, _) = listener.accept().await?;
let io = TokioIo::new(stream);
tokio::spawn(async move {
let _ = http1::Builder::new()
.serve_connection(io, service_fn(handle_request))
.await;
});
}
}Error Handling
use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Method, Request, Response, StatusCode};
use http_body_util::Full;
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use std::convert::Infallible;
use std::num::ParseIntError;
#[derive(Debug)]
enum ApiError {
NotFound,
ParseError(ParseIntError),
Internal(String),
}
impl std::fmt::Display for ApiError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ApiError::NotFound => write!(f, "Not Found"),
ApiError::ParseError(e) => write!(f, "Parse error: {}", e),
ApiError::Internal(s) => write!(f, "Internal error: {}", s),
}
}
}
fn error_response(error: ApiError) -> Response<Full<Bytes>> {
let (status, message) = match error {
ApiError::NotFound => (StatusCode::NOT_FOUND, "Resource not found"),
ApiError::ParseError(_) => (StatusCode::BAD_REQUEST, "Invalid parameter"),
ApiError::Internal(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error"),
};
Response::builder()
.status(status)
.body(Full::new(Bytes::from(message.to_string())))
.unwrap()
}
async fn handle_request(req: Request<hyper::body::Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
let path = req.uri().path();
let result: Result<Response<Full<Bytes>>, ApiError> = match (req.method(), path) {
(&Method::GET, path) if path.starts_with("/square/") => {
let num_str = path.trim_start_matches("/square/");
let num: i32 = num_str.parse().map_err(ApiError::ParseError)?;
let result = num * num;
Ok(Response::new(Full::new(Bytes::from(result.to_string()))))
}
(&Method::GET, "/") => {
Ok(Response::new(Full::new(Bytes::from("Welcome!"))))
}
_ => Err(ApiError::NotFound),
};
Ok(result.unwrap_or_else(error_response))
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let listener = TcpListener::bind("127.0.0.1:3000").await?;
println!("Try: http://127.0.0.1:3000/square/12");
loop {
let (stream, _) = listener.accept().await?;
let io = TokioIo::new(stream);
tokio::spawn(async move {
let _ = http1::Builder::new()
.serve_connection(io, service_fn(handle_request))
.await;
});
}
}Real-World REST API Example
use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{header, Method, Request, Response, StatusCode};
use http_body_util::{BodyExt, Full};
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;
use std::collections::HashMap;
use std::convert::Infallible;
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Todo {
id: u64,
title: String,
completed: bool,
}
type TodoStore = Arc<RwLock<HashMap<u64, Todo>>>;
#[derive(Deserialize)]
struct CreateTodo {
title: String,
}
fn json_response<T: Serialize>(data: T, status: StatusCode) -> Response<Full<Bytes>> {
let json = serde_json::to_string(&data).unwrap();
Response::builder()
.status(status)
.header(header::CONTENT_TYPE, "application/json")
.body(Full::new(Bytes::from(json)))
.unwrap()
}
async fn handle_todos(
req: Request<hyper::body::Incoming>,
store: TodoStore,
) -> Result<Response<Full<Bytes>>, Infallible> {
let path = req.uri().path().to_string();
let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
match (req.method(), segments.as_slice()) {
// GET /todos - list all
(&Method::GET, ["todos"]) => {
let todos = store.read().await;
let list: Vec<_> = todos.values().cloned().collect();
Ok(json_response(list, StatusCode::OK))
}
// GET /todos/:id - get one
(&Method::GET, ["todos", id]) => {
let id: u64 = match id.parse() {
Ok(id) => id,
Err(_) => return Ok(Response::builder()
.status(StatusCode::BAD_REQUEST)
.body(Full::new(Bytes::from("Invalid ID")))
.unwrap()),
};
let todos = store.read().await;
match todos.get(&id) {
Some(todo) => Ok(json_response(todo.clone(), StatusCode::OK)),
None => Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Full::new(Bytes::from("Todo not found")))
.unwrap()),
}
}
// POST /todos - create
(&Method::POST, ["todos"]) => {
let body = req.collect().await.unwrap().to_bytes();
let create: CreateTodo = match serde_json::from_slice(&body) {
Ok(c) => c,
Err(_) => return Ok(Response::builder()
.status(StatusCode::BAD_REQUEST)
.body(Full::new(Bytes::from("Invalid JSON")))
.unwrap()),
};
let mut todos = store.write().await;
let id = todos.keys().max().unwrap_or(&0) + 1;
let todo = Todo {
id,
title: create.title,
completed: false,
};
todos.insert(id, todo.clone());
Ok(json_response(todo, StatusCode::CREATED))
}
// DELETE /todos/:id
(&Method::DELETE, ["todos", id]) => {
let id: u64 = match id.parse() {
Ok(id) => id,
Err(_) => return Ok(Response::builder()
.status(StatusCode::BAD_REQUEST)
.body(Full::new(Bytes::from("Invalid ID")))
.unwrap()),
};
let mut todos = store.write().await;
match todos.remove(&id) {
Some(_) => Ok(Response::new(Full::new(Bytes::from("Deleted")))),
None => Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Full::new(Bytes::from("Todo not found")))
.unwrap()),
}
}
_ => {
Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Full::new(Bytes::from("Not Found")))
.unwrap())
}
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let listener = TcpListener::bind("127.0.0.1:3000").await?;
let store: TodoStore = Arc::new(RwLock::new(HashMap::new()));
println!("Todo API running at http://127.0.0.1:3000");
println!("Endpoints:");
println!(" GET /todos");
println!(" GET /todos/:id");
println!(" POST /todos");
println!(" DELETE /todos/:id");
loop {
let (stream, _) = listener.accept().await?;
let io = TokioIo::new(stream);
let store = Arc::clone(&store);
tokio::spawn(async move {
let _ = http1::Builder::new()
.serve_connection(io, service_fn(move |req| {
let store = Arc::clone(&store);
async move { handle_todos(req, store).await }
}))
.await;
});
}
}Summary
- Use
TcpListenerto accept connections, thenserve_connectionfor each service_fnwraps a closure that handlesRequest→ResponseResponse::builder()for building responses with status and headersBodyExt::collect()to read entire request body- Use
Arcto share state across handlers - Hyper 1.x requires
http-body-utilandhyper-utilcrates - Stream responses with
StreamBodyfor large or generated content - Implement middleware by wrapping handlers
- Use
tokio::spawnto handle each connection concurrently StatusCodefor HTTP status codesheadermodule contains standard header names- Perfect for: low-level HTTP services, building web frameworks, high-performance APIs, custom protocols
