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.
On this page
TypeScript interfaces, strings, and the URL class.
- Split the request line, headers, and body of an HTTP/1.1 message and read a query parameter.
- Predict the sample output and explain the state changes.
- Complete the practice task and check the boundary cases.
The concept
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.
Code example
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
Follow the code
Open the walkthrough to inspect the example one step at a time.
Show guided walkthrough
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 |
Behind the scenes
How TypeScript processes this code
TypeScript checks types and emits JavaScript. Type annotations are removed before execution.
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.