go-http — HTTP/1.1 Server & Client in Rust
A faithful port of Go's net/http library to Rust, built on go-lib for goroutine-style concurrency.
Status: Production-ready. All 98 tests passing. Full HTTP/1.1 support with TLS.
Features
Server
- Goroutine-per-connection model — each accepted connection spawns a lightweight goroutine
- Keep-Alive support — persistent connections reuse TCP sockets
- Chunked transfer encoding — streams large responses without buffering
- Handler routing —
ServeMuxwith longest-prefix matching - Middleware —
strip_prefix,file_server,timeout_handler - TLS — HTTPS via rustls with automatic certificate loading
Client
- Connection pooling — idle TCP connections reused per host
- Redirect following — automatic POST→GET on 301/302/303, respecting
max_redirects - Cookie jar — automatic cookie storage and transmission
- Timeout support — per-request deadline via go-lib context
HTTP
- RFC 7231 parsing — request/response line, headers, trailers
- Content negotiation — Content-Length vs Transfer-Encoding, 1xx/204/304 handling
- MIME types — detect, parse, format media types with RFC 2231 continuations
- Form encoding —
application/x-www-form-urlencodedclient support
Quick Start
Run the Hello Server
Then test it:
Run a Static File Server
Serves files from examples/assets/ under /static/:
Use the Client
Or in your own code:
use Client;
Examples
All examples run under go_lib::run() so goroutines and channels are available:
| Example | Purpose |
|---|---|
hello_server |
Minimal HTTP server with query parameter handling |
echo_server |
Echoes request method, path, headers back as JSON |
routing |
ServeMux pattern matching (exact vs prefix) |
static_files |
File serving with prefix stripping |
client_get |
HTTP client library usage |
middleware |
Composable handler middleware (logging + timeout) |
Run with: cargo run --example <name>
API Reference — Go → Rust
| Go | Rust |
|---|---|
http.Server |
go_http::server::Server |
http.ListenAndServe |
go_http::server::listen_and_serve |
http.ListenAndServeTLS |
go_http::server::listen_and_serve_tls |
http.Handler |
go_http::handler::Handler |
http.HandlerFunc |
go_http::handler::handler_func |
http.ServeMux |
go_http::handler::ServeMux |
http.StripPrefix |
go_http::handler::strip_prefix |
http.FileServer |
go_http::handler::file_server |
http.TimeoutHandler |
go_http::handler::timeout_handler |
http.Client |
go_http::client::Client |
http.Transport |
go_http::client::Transport |
http.RoundTripper |
go_http::client::RoundTripper |
http.Request |
go_http::request::Request |
http.Response |
go_http::response::Response |
http.Cookie |
go_http::cookie::Cookie |
io.Reader |
std::io::Read |
io.Writer |
std::io::Write |
net.Conn.Read / Write |
TcpStream: Read + Write |
net.Listener |
go_lib::net::TcpListener |
context.Context |
go_lib::context::Context |
sync.Mutex |
std::sync::Mutex |
Architecture
Core Modules
server.rs— HTTP server,listen_and_serve, goroutine-per-connection loopclient.rs— HTTP client,Transportwith connection pooling, redirect handlinghandler.rs—Handlertrait,ServeMuxrouting, middleware wrappersrequest.rs—Requestvalue type with URL, headers, body, contextresponse.rs— Response types,ConnResponseWriterparse/— HTTP parsing (request, response, chunked encoding, transfer encoding)mime/— MIME type detection, media-type parsing, multipart, quoted-printableheader.rs— Case-insensitive HTTP header mapcookie.rs— Cookie struct,CookieJartrait,MemoryCookieJarimplementationtls.rs— TLS config helpers (server_config,default_client_config)util.rs— Helpers (HTTP-date parsing, content-type detection,set_cookie,redirect)
Concurrency Model
Each Server spawns an accept goroutine that receives connections. For each new connection, a handler goroutine is spawned:
go!;
The handler goroutine runs a keep-alive loop:
- Parse request headers via
read_request(...) - Attach body reader (chunked or content-length)
- Dispatch to handler via
handler.serve_http(&mut w, &req) - Write response headers and body via
w - Loop for next request or close on
Connection: close
All blocking I/O (reads, writes, accepts) is transparent to go-lib's netpoll — the scheduler parks goroutines and resumes them when I/O is ready, without blocking OS threads.
Building & Testing
Run All Tests
This runs:
- 77 unit tests across all modules
- 13 integration tests in
tests/server_client.rs(GET, POST, redirects, cookies, routing, etc.) - 8 integration tests in
tests/middleware.rs(strip_prefix, file_server, timeout_handler)
Test Structure
Integration tests spawn a server goroutine and client goroutine in the same go_lib::run() call — no separate OS threads. This avoids netpoll corruption from concurrent scheduler instances.
Each integration test file uses a static NET_LOCK: Mutex<()> to serialize tests (netpoll is a process-global singleton).
Build Examples
Check Build (no tests)
TLS Support
Server (HTTPS)
Load a certificate and key, then call listen_and_serve_tls:
use listen_and_serve_tls;
use handler_func;
Client (HTTPS)
By default, the client trusts the Mozilla root CA bundle:
let mut client = new;
let resp = client.get.unwrap;
To add a custom CA:
let tls_cfg = client_config_with_ca.unwrap;
let mut transport = new;
transport.tls_config = Some;
let mut client = new;
client.transport = new;
Certificates are loaded via rustls-pemfile and must be in PEM format.
Dependencies
| Crate | Version | Purpose |
|---|---|---|
go-lib |
0.5.1 | Goroutines, channels, context, netpoll TCP |
go-lib-macros |
0.5.1 | go! and select! macros |
url |
2 | URL parsing and manipulation |
base64 |
0.22 | Base64 encoding/decoding |
rustls |
0.23 | TLS client & server |
rustls-pemfile |
2 | PEM file parsing |
webpki-roots |
0.26 | Mozilla root CA bundle |
Design Notes
Why impl<H: Handler> Handler for Arc<H>?
This allows middleware to stack Arc<ServeMux> directly without wrapper boilerplate:
let mux = new;
let with_timeout = timeout_handler;
Why impl Read + Write for &TcpStream (in addition to &mut)?
TcpStream::read and TcpStream::write only touch goroutine state, not the struct itself. This is safe with & because all blocking is managed by go-lib's netpoll — there are no interior mutability hazards.
Why try_clone() instead of Rc<RefCell<TcpStream>>?
TcpStream::try_clone() uses dup(2) (Unix) or DuplicateHandle (Windows) to create independent file descriptors pointing to the same kernel socket. This avoids shared ownership and borrow checker complexity.
In serve_conn, the request body and response writer operate on independent clones — true parallelism without contention.
Separate Integration Test Binaries
Each .rs file in tests/ compiles to its own binary with its own OS process, scheduler, and netpoll instance. This isolation prevents goroutine pointer collisions when tests run in parallel.
Contributing
- Follow the Rust API Guidelines
- Add tests for new features (unit tests in
src/, integration tests intests/) - Run
cargo testbefore submitting - Port behavior from Go's
net/httpwhen possible for API familiarity
License
Licensed under the Apache License, Version 2.0 or the MIT License, at your option.
Go Reference
For behavior clarification, consult the Go net/http documentation.