TypeScript: parsing a raw HTTP request into a typed object

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.

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);
Read the full lesson