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.

TypeScript
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);
Output
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
Guided walkthroughReady
1interface HttpRequest {
2 method: string;
3 path: string;
4 version: string;
5 headers: Record<string, string>;
6 body: string;
7}
8
9function parse(raw: string): HttpRequest {
10 const split = raw.indexOf("\r\n\r\n");
11 const head = split === -1 ? raw : raw.slice(0, split);
12 const body = split === -1 ? "" : raw.slice(split + 4);
13 const [requestLine, ...headerLines] = head.split("\r\n");
14 const [method, path, version] = requestLine.split(" ");
15 const headers: Record<string, string> = {};
16 for (const line of headerLines) {
17 const colon = line.indexOf(":");
18 headers[line.slice(0, colon).trim().toLowerCase()] = line.slice(colon + 1).trim();
19 }
20 return { method, path, version, headers, body };
21}
22
23const raw = "POST /search?q=tcp&page=2 HTTP/1.1\r\nHost: example.com\r\nContent-Length: 5\r\n\r\nhello";
24const request = parse(raw);
25console.log(request.method + " " + request.path + " " + request.version);
26console.log("host: " + request.headers["host"]);
27const url = new URL(request.path, "http://" + request.headers["host"]);
28console.log("q=" + url.searchParams.get("q") + " page=" + url.searchParams.get("page"));
29console.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
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput 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.

Further reading