Step 1 of 5 · Reading · ~4 min
Read
Production Features
Putting It All Together
Every piece you have built so far is a function on strings. This lesson is about the wiring: the order those functions run in, what each one is allowed to assume about the last, and the handful of things a real deployment needs that no single lesson owned. The shape below is not a simplification of nginx — it is nginx, and Apache, and Go's net/http, with the names changed.
The pipeline
listen(port)
loop:
conn = accept()
spawn handle_connection(conn):
while True:
request = parse_request(conn) # request line, headers, framed body
if request is None: break # EOF, or malformed => 400 and stop
handler = router.match(request) # exact path, then method => 404 / 405
response = handler(request)
response.headers["Date"] = now_rfc1123()
response.headers["Server"] = "MyServer/1.0"
send_response(conn, response) # status line, headers, CRLF, body
access_log(request, response)
if should_close(request, response): break
close(conn)
The ordering is not stylistic. Parsing must complete — body included — before routing, because a handler that never reads the body leaves those bytes in the socket for the next iteration to mis-parse as a request line. Date and Server are set after the handler runs so a handler can override them and the framework still guarantees they exist. The log line is written after the response is sent, so it can record the real status and byte count rather than the intended ones. And should_close is consulted last because both the request's Connection header and anything that went wrong while responding get a vote.
Errors have to be part of the pipeline
The single biggest gap between a working server and a shippable one is what happens when a handler raises. Wrap the handler call: an unexpected exception becomes 500, logged with a stack trace on your side and a bare explanation on the wire. Never let the exception text reach the client — internal paths, SQL fragments and library versions in an error page are reconnaissance handed over for free.
The distinction from the errors lesson does real work here. A parse failure is a 400 and the client's problem; a handler crash is a 500 and yours. If they land in the same bucket, your error rate graph stops meaning anything, because it moves when a scanner probes you with garbage — and the one alert that should wake somebody up now cries wolf.
Limits, or a one-packet denial of service
Everything that reads from a socket needs a bound, because the default of "read until it ends" is a promise the client has no reason to keep:
- Request line ~8 KB, each header ~8 KB, header count ~100. Without a cap, one connection dribbling out an endless header line consumes memory until the process dies. That attack has a name — Slowloris — and it costs the attacker almost nothing.
- Body size, configurable per route, answered with
413. Uploads need a larger bound than a JSON API; neither needs an unlimited one. - Timeouts on read, on write, on idle, and on total request duration. A connection that is open but silent is indistinguishable from a working one until you time it out.
Each limit is a fixed number and a clear status code, and together they are the difference between "degrades under attack" and "falls over".
Graceful shutdown
On SIGTERM: stop accepting new connections, let in-flight requests finish, then exit — with a deadline, after which the remainder is dropped. This is what makes a deploy invisible. A load balancer takes the instance out of rotation and the requests already in flight complete normally instead of turning into failed responses in a user's browser. A server that dies instantly on SIGTERM produces a burst of 502s on every single deploy, which teams often spend months misattributing to the network.
What you have not built
HTTPS in production is usually terminated ahead of you by a load balancer, which then speaks plain HTTP to this server on a private network. HTTP/2 reuses every semantic here — methods, status codes, headers — but replaces the text framing entirely with binary frames multiplexed over one connection, and HTTP/3 moves the whole thing onto QUIC over UDP. What you built is HTTP/1.1, which is still what the far side of most load balancers speaks, and it is the layer whose vocabulary all the others kept.
Your exercise: End-to-End Request Pipeline
Routes and requests in; the dispatch result plus its access-log line out, for each request in order. It is the pipeline compressed to its decisions: strip the query string, look up (method, path), and pick between the handler, a 405 when the path exists under a different verb, and a 404 when it does not — then log the status you actually returned. The ladder covers a plain hit, a request with a query string, a body whose length must be counted, a path registered only for another method, and an unregistered path. Two outputs per request, and the second one has to agree with the first.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…