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
split_once("\r\n\r\n")separates the head from the body at the first blank line;?returnsNoneif there isn't one.- The first line of the head is the request line; the rest are headers (ignored here).
- An enum for the method means the rest of the program can't see a misspelled
"GETT": invalid methods are rejected at the edge, once.
Common mistakes
- Splitting on
\nonly. HTTP uses\r\n; splitting on\nleaves a\rat the end of every line and"HTTP/1.1\r" != "HTTP/1.1". - Treating methods as case-insensitive.
getis not a valid method; methods are case-sensitive (header names, on the other hand, are case-insensitive). - Counting characters for
Content-Length. It's the number of bytes of the body:"é"is 2 bytes, so use.len(), not.chars().count().
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.