🎓 A1Learner
🔍

Rust HTTP by Hand

HTTP is plain text on a TCP stream. A request is a few lines written to the stream, and an answer is a few lines read back and parsed.

The whole request, as text

This one string is a complete request. Nothing else is needed:

let request = "GET /hello HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n";

Send it, then read to the end

Connection: close makes the server hang up after answering. The read then has a clear end:

client.write_all(request.as_bytes()).unwrap();
let mut answer = String::new();
client.read_to_string(&mut answer).unwrap();

Split the head from the body

The blank line is the border. Everything before it is head. Everything after it is body:

let split = answer.find("\r\n\r\n").unwrap();
let head = &answer[..split];
let body = &answer[split + 4..];
println!("body = {}", body);

Read the status line

The first line of the head carries the verdict. Its second word is the status code:

let status_line = head.lines().next().unwrap();
println!("status line = {}", status_line);
let code: u32 = status_line.split(' ').nth(1).unwrap().parse().unwrap();
println!("code = {}", code);

Read the headers into pairs

Each remaining head line splits at its first colon:

for line in head.lines().skip(1) {
    let (name, value) = line.split_once(':').unwrap();
    println!("header {} = {}", name, value.trim());
}

Example

use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::thread;

const RESPONSE: &str = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 14\r\nConnection: close\r\n\r\nHello, Ferris!";

fn main() {
    let listener = TcpListener::bind("127.0.0.1:0").unwrap();
    let addr = listener.local_addr().unwrap();
    let server = thread::spawn(move || {
        let (mut stream, _) = listener.accept().unwrap();
        let mut request = Vec::new();
        let mut byte = [0u8; 1];
        while !request.ends_with(b"\r\n\r\n") {
            stream.read_exact(&mut byte).unwrap();
            request.push(byte[0]);
        }
        let text = String::from_utf8(request).unwrap();
        println!("server got = {}", text.lines().next().unwrap());
        stream.write_all(RESPONSE.as_bytes()).unwrap();
    });

    let mut client = TcpStream::connect(addr).unwrap();
    let request = "GET /hello HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n";
    client.write_all(request.as_bytes()).unwrap();
    let mut answer = String::new();
    client.read_to_string(&mut answer).unwrap();

    let split = answer.find("\r\n\r\n").unwrap();
    let head = &answer[..split];
    let body = &answer[split + 4..];
    let mut lines = head.lines();
    let status_line = lines.next().unwrap();
    println!("status line = {}", status_line);
    let code: u32 = status_line.split(' ').nth(1).unwrap().parse().unwrap();
    println!("code = {}", code);
    for line in lines {
        let (name, value) = line.split_once(':').unwrap();
        println!("header {} = {}", name, value.trim());
    }
    println!("body = {}", body);
    server.join().unwrap();
}

Key points

  • An HTTP request is text: one request line, some header lines, one blank line.
  • A body follows only when the request carries one
  • The request line names the method, the path and the version.
  • It looks like GET /hello HTTP/1.1
  • The Host header is required in HTTP/1.1.
  • It names the site you mean when one machine hosts many
  • Header lines end with \r\n.
  • A lone \r\n — the blank line — ends the head of the message
  • Send Connection: close and the server hangs up after answering.
  • read_to_string then collects the whole answer

In this example, the server is a thread with a canned answer. It reads bytes until the blank line arrives. It prints your request line, and it sends the answer back. The client writes the hand-made request and reads to the end of the stream. Then plain text tools do the parsing. Find the blank line. Split head from body. Read the code out of the status line. Split each header at its colon. No crate took part. This is the work reqwest does for you, written out longhand.

Note

A 404 answer is still a well-formed HTTP answer.

The status line reports the server's verdict.

It is not a transport failure.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

Why does the request end with a blank line?