RustArena

Step 1 of 7

HTTP in one page

A web API is a program that waits for requests over the network and sends back responses, using the HTTP protocol. Your browser, a phone app, curl and other services are the clients; your program is the server. Underneath every framework, HTTP/1.1 is just text. A client that creates a score sends:

POST /scores HTTP/1.1                 <- request line: method, path, version
Host: arena.dev                       <- headers, one "Name: value" per line
Content-Type: application/json
Content-Length: 26
                                      <- an empty line ends the headers
{"player":"ana","score":7}            <- the body (optional)

and the server answers:

HTTP/1.1 201 Created                  <- status line: version, status code, reason
Content-Type: application/json
Content-Length: 34

{"id":12,"player":"ana","score":7}

Lines end with \r\n (carriage return + newline), and the blank line is \r\n\r\n.

Methods say what to do

Method Meaning Body?
GET read a resource; never changes anything no
POST create something new (or trigger an action) yes
PUT replace a resource completely yes
PATCH change some fields of a resource yes
DELETE remove a resource no

The path names the resource: /scores is the collection, /scores/12 one item in it.

Status codes say how it went

Code Name When
200 OK it worked, here's the result
201 Created a POST created something
204 No Content it worked, nothing to send back (typical for DELETE)
400 Bad Request the request is malformed (bad JSON, bad id)
404 Not Found no such resource or path
405 Method Not Allowed the path exists, but not with that method
409 Conflict clashes with current state (duplicate name, not enough money)
422 Unprocessable Content well-formed, but the data breaks a rule
500 Internal Server Error the server's fault

The first digit is what clients check first: 2xx success, 4xx the client's mistake, 5xx the server's.

Modeling it in Rust

Frameworks (like axum, at the end of this module) parse the text for you, but knowing what's underneath makes you much better at debugging (curl -v shows exactly this text). Here is the core of a request parser:

#[derive(Debug, PartialEq)]
enum Method {
    Get,
    Post,
}

struct Request {
    method: Method,
    path: String,
    body: String,
}

fn parse_request(raw: &str) -> Option<Request> {
    let (head, body) = raw.split_once("\r\n\r\n")?;
    let request_line = head.split("\r\n").next()?;
    let mut parts = request_line.split(' ');
    let method = match parts.next()? {
        "GET" => Method::Get,
        "POST" => Method::Post,
        _ => return None,
    };
    let path = parts.next()?.to_string();
    Some(Request { method, path, body: body.to_string() })
}

fn main() {
    let raw = "POST /scores HTTP/1.1\r\nHost: arena.dev\r\nContent-Type: application/json\r\n\r\n{\"player\":\"ana\",\"score\":7}";
    if let Some(req) = parse_request(raw) {
        println!("{:?} {} body={}", req.method, req.path, req.body);
    }
    let get = parse_request("GET /scores/12 HTTP/1.1\r\nHost: arena.dev\r\n\r\n").unwrap();
    println!("{:?} {} body is empty: {}", get.method, get.path, get.body.is_empty());
    println!("{}", parse_request("BREW /coffee HTTP/1.1\r\n\r\n").is_none());
}
Post /scores body={"player":"ana","score":7}
Get /scores/12 body is empty: true
true

Common mistakes

Your task

Write parse_request for all four methods GET, POST, PUT and DELETE. Return None unless: there's a \r\n\r\n separator, the request line has exactly three parts separated by spaces, the method is one of the four (uppercase), the path starts with /, and the version is HTTP/1.1. The body is everything after the separator (possibly empty).

Challenge 2

Write fn parse_headers(head: &str) -> BTreeMap<String, String>. head is the request line followed by header lines, separated by \r\n. Skip the request line; for each Name: value line, store the name lowercased (header names are case-insensitive) and the value trimmed. Split at the first colon only (Host: localhost:8080 has a colon in the value). Ignore lines without a colon.

Challenge 3

Write the other direction: fn to_http(status: u16, body: &str) -> String builds a full HTTP response: the status line HTTP/1.1 <code> <reason>, a Content-Type: application/json header, a Content-Length header with the body's length in bytes, a blank line, then the body. Lines end with \r\n. Reasons: 200 OK, 201 Created, 204 No Content, 400 Bad Request, 404 Not Found, 405 Method Not Allowed, 409 Conflict, 500 Internal Server Error, anything else Unknown.

Next: Routing by method and path