TypeScript: parsing a raw HTTP request into a typed object
Split the request line, headers, and body of an HTTP/1.1 message and read a query parameter.
interface HttpRequest {
method: string;
path: string;
version: string;
headers: Record<string, string>;
body: string;
}
function parse(raw: string): HttpRequest {
const split = raw.indexOf("\r\n\r\n");
const head = split === -1 ? raw : raw.slice(0, split);
const body = split === -1 ? "" : raw.slice(split + 4);
const [requestLine, ...headerLines] = head.split("\r\n");
const [method, path, version] = requestLine.split(" ");
const headers: Record<string, string> = {};
for (const line of headerLines) {
const colon = line.indexOf(":");
headers[line.slice(0, colon).trim().toLowerCase()] = line.slice(colon + 1).trim();
}
return { method, path, version, headers, body };
}
const raw = "POST /search?q=tcp&page=2 HTTP/1.1\r\nHost: example.com\r\nContent-Length: 5\r\n\r\nhello";
const request = parse(raw);
console.log(request.method + " " + request.path + " " + request.version);
console.log("host: " + request.headers["host"]);
const url = new URL(request.path, "http://" + request.headers["host"]);
console.log("q=" + url.searchParams.get("q") + " page=" + url.searchParams.get("page"));
console.log("body (" + request.headers["content-length"] + " bytes): " + request.body);POST /search?q=tcp&page=2 HTTP/1.1 host: example.com q=tcp page=2 body (5 bytes): hello
Explanation
An HTTP/1.1 request is plain text. The first line is the request line with the method, the target path, and the protocol version. Header lines follow, each as name: value, and a blank line ends the headers. Lines are separated by carriage return plus line feed (CRLF), and the body, if any, comes after the blank line.
The parser splits the head from the body at the first blank line, then splits the head into lines. Header names are case-insensitive, so they are stored in lower case. The URL class parses the query string. An interface describes the result, so the rest of the program can rely on its shape.
Explore the dry run
interface HttpRequest { method: string; path: string; version: string; headers: Record<string, string>; body: string;} function parse(raw: string): HttpRequest { const split = raw.indexOf("\r\n\r\n"); const head = split === -1 ? raw : raw.slice(0, split); const body = split === -1 ? "" : raw.slice(split + 4); const [requestLine, ...headerLines] = head.split("\r\n"); const [method, path, version] = requestLine.split(" "); const headers: Record<string, string> = {}; for (const line of headerLines) { const colon = line.indexOf(":"); headers[line.slice(0, colon).trim().toLowerCase()] = line.slice(colon + 1).trim(); } return { method, path, version, headers, body };} const raw = "POST /search?q=tcp&page=2 HTTP/1.1\r\nHost: example.com\r\nContent-Length: 5\r\n\r\nhello";const request = parse(raw);console.log(request.method + " " + request.path + " " + request.version);console.log("host: " + request.headers["host"]);const url = new URL(request.path, "http://" + request.headers["host"]);console.log("q=" + url.searchParams.get("q") + " page=" + url.searchParams.get("page"));console.log("body (" + request.headers["content-length"] + " bytes): " + request.body);Follow the example
Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.
View the complete dry-run table
| Step / line | What happens | State after step | Output so far |
|---|---|---|---|
| A blank line (CRLF CRLF) separates the headers from the body. | — | — | |
| The request line has three space-separated parts. | — | — | |
| Names are lower-cased because HTTP header names are case-insensitive. | — | — | |
| The request line is recovered. | — | POST /search?q=tcp&page=2 HTTP/1.1 | |
| Headers are looked up by lower-case name. | — | POST /search?q=tcp&page=2 HTTP/1.1 host: example.com | |
| URL decodes the query string. | — | POST /search?q=tcp&page=2 HTTP/1.1 host: example.com q=tcp page=2 | |
| Content-Length says the body has 5 bytes, which matches. | — | POST /search?q=tcp&page=2 HTTP/1.1 host: example.com q=tcp page=2 body (5 bytes): hello |
Cost and common mistakes
O(m) for a message of m characters.
- Real parsers must handle malformed input, such as a missing colon, huge headers, or repeated headers, and never trust Content-Length without checking.
- Do not write your own HTTP parser for production. This example only shows the message format.
- Splitting on \r\n is correct for HTTP. Using \n alone would leave stray carriage returns in the values.
Try it yourself
How would you read the Content-Type header?
Reveal answer and reasoning
request.headers["content-type"], using the lower-case name.