Part 10 · 2 chapters · ~18 min

M10: HTTP Server

HTTP/1.1 on raw TCP: an incremental parser for pipelined requests and chunked bodies with limits, middleware and routing with 404 versus 405, responses chained in request order, chunked streaming, keep-alive rules and idle timeouts, and static files with ETags and traversal protection.

20

HTTP/1.1 on a byte stream

framing, ordering, persistence, streaming
  1. Bytes arrive with no boundaries, sometimes several requests in one read.
  2. Parsing: headers end at a blank line, and the body is framed by Content-Length or chunks.
  3. Routing through middleware, with 404 and 405 kept distinct.
  4. Responses go out in request order, which is HTTP/1.1's head-of-line blocking.
  5. Streaming uses chunked transfer encoding.
  6. Keep-alive, timeouts and limits.
where this connects
The Browser course part 1 covers the client side of the same wire: connections, HTTP/2 and HTTP/3, priorities and caching headers. The Cloud course part 2 covers what sits in front of a server like this: the load balancer that terminates TLS and the keep-alive timeouts that cause 502s. An app keep-alive timeout shorter than the load balancer's idle timeout is the classic cause of those 502s, and this server's 5-second idle timeout is exactly the setting involved.
TINY-HTTP: BYTES ON A SOCKET
two pipelined requests on one keep-alive connection, parsed incrementally and answered in order, one of them streamed
swipe the figure sideways, or tap expand for full screen
1/6
the bytes
The bytes: "GET /transfers/tr_1 HTTP/1.1\r\nHost: x\r\n\r\nGET /prices/stream HTTP/1.1\r\nHost: x\r\n\r\n" arrives in one TCP read: two pipelined requests. Another time the same bytes could arrive one character per read; the parser cannot assume either.
21

Building it: tiny-http

Repo: repos/http. Parser, server, router, static files and an example app.

code
// src/parser.ts: chunked bodies, waiting for more bytes whenever a piece is incomplete
for (;;) {
  const lineEnd = this.buf.indexOf('\r\n', pos);
  if (lineEnd === -1) return null;                                   // size line not here yet
  const size = parseInt(this.buf.toString('latin1', pos, lineEnd).split(';')[0], 16);
  if (Number.isNaN(size)) throw new HttpError(400, 'bad chunk size');
  if (this.buf.length < lineEnd + 2 + size + 2) return null;          // chunk data not here yet
  if (size === 0) { pos = lineEnd + 4; break; }
  parts.push(this.buf.subarray(lineEnd + 2, lineEnd + 2 + size));
  pos = lineEnd + 2 + size + 2;
}
code
// src/server.ts: pipelined requests answered in order by chaining
for (const req of reqs) {
  const keepAlive = req.version === 'HTTP/1.1' ? conn !== 'close' : conn === 'keep-alive';
  chain = chain.then(async () => {                    // the next response waits for this one
    const res = new Response(sock, keepAlive, req.method === 'HEAD');
    try { await handler(req, res); if (!res.finished) res.end(); }
    catch (e) { /* 4xx from HttpError, else 500 */ }
  });
}
code
$ npm start
$ curl -N localhost:8080/prices/stream          # lines arrive 20 ms apart
$ printf 'GET / HTTP/1.1\r\nHost: x\r\n\r\nGET /nowhere HTTP/1.1\r\nHost: x\r\n\r\n' | nc localhost 8080
HTTP/1.1 200 OK … tiny-http: try …
HTTP/1.1 404 Not Found …
Run it. In repos/http: npm test, then npm start. Use curl -v against localhost:8080/static/index.html twice, the second time with -H 'If-None-Match: …' carrying the ETag, and you get a 304. Pipeline with nc as shown above. Then open the Network panel (the DevTools course part 4) on localhost:8080/static/index.html and read the headers this server wrote by hand.
exercises
1. Range requests (206 Partial Content) for media seeking. 2. Compression, negotiating gzip or Brotli with Accept-Encoding. 3. Slowloris defence: a deadline for the headers to finish arriving. 4. TLS with node:tls. 5. HTTP/2: parse the connection preface and SETTINGS frames, then multiplex two streams.
the course, complete
Ten systems: a cache, a log, a compiler, a transpiler, two frameworks, a bundler, a kernel, a database engine and a web server, every one tested and every one runnable. Together they cover most of the stack under a modern web product, built from the inside.