loading
loading
HTTP is just text over a socket -- until you have to parse it yourself. Build the core of a real HTTP/1.1 server in four stages and stop treating load balancers and gateways as magic boxes.
HTTP is the most-used protocol in the world, yet most engineers only know it via Express, FastAPI, or Rails -- never the raw bytes. This matters more than it sounds: every load balancer, reverse proxy, API gateway, and CDN exists because of specific, exploitable properties of raw HTTP. Request smuggling attacks, Keep-Alive connection exhaustion, Content-Length spoofing, header injection -- these are all consequences of how HTTP is parsed, and they're invisible if you've only ever worked at the framework level. After this track, when you configure nginx's keepalive_timeout or debug a 400 Bad Request from your ALB, you'll know exactly which byte in the request caused it. You'll also understand why HTTP/2 multiplexing was a breakthrough -- because you'll have felt the pain of HTTP/1.1 head-of-line blocking firsthand.
A working HTTP/1.1 request parsing pipeline and router in four stages:
Stage 1 -- Request Parser: Takes a raw HTTP/1.1 request string and returns a parsed object with method, path, headers, and body. Handles header folding, case-insensitive header names (per RFC 9112), and query string decoding.
Stage 2 -- Router: Takes a parsed request and a route table (with named parameters like /users/:id) and returns the matched handler plus extracted params. The router you'll build is structurally identical to the routing core of Express and Hono.
Stage 3 -- Response Framing: Takes a handler result and frames it as a correctly-formatted HTTP/1.1 response with status line, headers, CRLF separator, and body. Content-Length must be exact -- one wrong byte and the client's TCP parser hangs waiting for bytes that will never arrive.
Stage 4 -- Keep-Alive: Instead of closing the TCP connection after each response, reuse it for subsequent requests. This is how every HTTP connection pool works, and why your browser can load 100 assets from one connection.
HTTP/1.1 is plain ASCII text over a TCP socket. The full wire format of a request is:
`` METHOD /path HTTP/1.1\r\n Header-Name: Header-Value\r\n Header-Name: Header-Value\r\n \r\n body bytes (exactly Content-Length of them, or until connection close) `
The \r\n (CRLF) is specified in RFC 9112 and has been the source of security bugs for 30 years. Many servers accept \n alone (lenient parsing); attackers exploit differences between a frontend proxy's lenient parser and a backend's strict parser to inject requests -- this is HTTP request smuggling.
Header parsing rules (RFC 9112 §5): Header names are case-insensitive -- Content-Type and content-type are the same header. Header values may contain colons -- Authorization: Bearer abc:def:ghi is valid; you split on the first colon only. Whitespace around the value is trimmed. In Stage 1 you'll implement exactly these rules.
Routing: A route like /users/:id/posts/:postId is a pattern. The router must parse it into segments, match each segment of the incoming path, and extract named captures. A linear scan through routes is O(n) -- fine for < 1,000 routes. Production routers (Radix tree in Hono, trie in nginx) are O(log n) but structurally identical at the matching level.
Response framing: The status line is HTTP/1.1 {status} {reason}\r\n. Every response must include Content-Length` if the body is non-empty and the connection is being kept alive -- without it, the client doesn't know where the response ends and the next one begins. Getting this wrong is how you create a connection that hangs forever after the first request.
Keep-Alive: HTTP/1.1 defaults to persistent connections (Keep-Alive). The client sends multiple requests on the same socket, back-to-back or pipelined. Your server must correctly delimit where each request ends (using Content-Length for requests with bodies) and where each response ends (using Content-Length in the response). This is the foundation of every connection pool -- the "pool" is just a collection of sockets in Keep-Alive mode waiting for the next request.
Turn a raw HTTP/1.1 request string into a structured object. Parse the request line (method, path, version), headers (case-insensitive, colon-split), query string, and body. This is what nginx, Node.js http, and every reverse proxy does before your application code ever runs.
Match the parsed path against a route table with named parameters like `/users/:id`. Return the matched handler and extracted params. Build correct 404 vs 405 semantics: the path exists but wrong method is a 405, not a 404. Structurally identical to Express.js routing internals.
Serialize a handler result into a correctly-formatted HTTP/1.1 response with status line, headers, CRLF separator, and body. Content-Length must match the body byte length exactly -- one byte off and the client's TCP parser deadlocks waiting for the rest.
Don't close the connection after each response. Parse the next request from the same socket, send the next response, repeat. This is the foundation of every HTTP connection pool and multiplexer. After this stage you understand what nginx worker_connections actually manages.
nginx's HTTP parser handles the same CRLF parsing, header case folding, and Content-Length validation you implement in Stage 1 and 3. nginx's keepalive_timeout and keepalive_requests are the production versions of your Stage 4 connection reuse logic.
The http module you use in every Node.js server is a thin wrapper over llhttp (formerly http_parser), a C library doing exactly what Stage 1 does. When you require('http').createServer(), the callback you write receives the parsed result of Stage 1's output.
Express's routing layer (path-to-regexp) and Hono's radix router both implement the named-parameter matching you build in Stage 2. The pattern `/:id` you'll parse is identical in syntax to all three frameworks.
Load balancers implement the exact same request parsing and response framing to proxy HTTP traffic. The "400 Bad Request" you get from an ALB when your service returns malformed headers is the Stage 3 framing logic in reverse.
Ready to build?
Your code runs in the browser — no setup needed.