beprodready
Build Your Own HTTP Server

Stage 1: Parse the Request

stage 1 of 4 · ~20 min · runs in your browser

Objective:Understand the HTTP/1.1 wire format at the byte level — the foundation every web framework sits on top of.

HTTP/1.1 request formatCRLF line endingsheader case-insensitivity (RFC 9112)query string decodingpercent-encoding

A raw HTTP/1.1 request is lines of text separated by \r\n, a blank line (\r\n\r\n), then an optional body. Before any framework existed, someone wrote this function — now it's your turn.

Implement parseRequest(raw) returning:

  • method — e.g. "GET" (first token of the request line)
  • path — e.g. "/users/42" (without the query string)
  • query — object of decoded query params (?q=db%20engine&page=2{ q: "db engine", page: "2" }); {} when absent
  • headers — object with lowercased header names (header names are case-insensitive per RFC 9112; values keep their case, surrounding whitespace trimmed)
  • body — everything after the first blank line, "" if none

Structure of an HTTP/1.1 request:

METHOD /path?query HTTP/1.1\r\n
Header-Name: value\r\n
Another-Header: value\r\n
\r\n
body (optional)

The blank line is always exactly \r\n\r\n — one CRLF after the last header. The body can itself contain blank lines; only the first \r\n\r\n separates headers from body.

Why this matters in production

Nginx parses millions of these per second. Express, Fastify, and every other Node.js framework call something structurally identical to what you're about to write. When a malformed request causes a 400 or a header injection slips through, this is the code that either catches it or doesn't.

tests (6)

parses the request line

lowercases header names and trims values

splits and decodes the query string

captures the body

body may itself contain blank lines

header value with colon

parseRequest.js