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
- Bytes arrive with no boundaries, sometimes several requests in one read.
- Parsing: headers end at a blank line, and the body is framed by Content-Length or chunks.
- Routing through middleware, with 404 and 405 kept distinct.
- Responses go out in request order, which is HTTP/1.1's head-of-line blocking.
- Streaming uses chunked transfer encoding.
- 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.