Step 1 of 5 · Reading · ~3 min
Read
Production Features
Putting It All Together
Every piece you have built so far is a function on bytes. This lesson is the wiring: the order those functions run in, what each may assume about the last, and what a deployment needs that no single lesson owned. Your program will read everything one client wrote on one connection and write back exactly the bytes a real server would send, response after response.
The pipeline, one request at a time
- Request line (lesson 1): three fields, shape rules, then
501for an unknown method and505for an unsupported version. - Headers (lesson 2):
name: valuelines up to the empty line, lowercased names, trimmed values. A malformed line is400. HTTP/1.1 needs exactly oneHost. - Body framing (lesson 3):
Content-Lengthorchunked, never both. A bad length or bad chunk is400. If the bytes run out before the message is complete, the client is gone: there is nothing to answer, so stop. - Route (lessons 5 and 6): pick the handler, or
404, or405withAllow. - Response (lesson 4): build status line, headers,
Content-Length, body. HEAD gets the headers and no body. - Connection (lesson 7): decide keep or close. A closing response carries
Connection: close, and after it you read no more.
The order is not stylistic. The body must be fully consumed before the next request is parsed, or its tail becomes the next request line. Framing errors are decided before routing. The close decision comes last because the request's own headers, the version and the request counter all vote. Any error that is the client's fault ends the connection with Connection: close: after a parse failure you no longer know where the next request starts.
Errors belong in the pipeline
The gap between a working server and a shippable one is what happens when something goes wrong. A parse failure is the client's 400. A handler crash is your 500 and must never leak internals to the client. If they share a bucket your error-rate graph moves whenever a scanner sends garbage.
The built-in application
To make the output testable the server carries a small fixed application. All text bodies are text/plain; charset=utf-8.
| Request | Response |
|---|---|
GET /, HEAD / | 200, body Welcome to tinyhttp and a newline |
GET /user-agent | 200, the User-Agent value (empty if absent) |
GET /echo/<text> | 200, the percent-decoded segment <text> (non-empty, bad escape is 400) |
POST /echo | 200, the request body, whichever framing carried it |
GET /files/<name> | the in-memory file hello.txt (Hello, files! + newline, text/plain) or data.json ({"ok":true}, application/json); a decoded name containing / is 403, an unknown name 404 |
HEAD works wherever GET does. Unknown paths are 404. A known path with another method is 405 with Allow (GET, HEAD or, for /echo, POST). Every non-file error body is the reason phrase and a newline.
Wire format of each response
HTTP/1.1 <code> <reason>
Content-Type: ...
Allow: ... (405 only)
Content-Length: <bytes>
Connection: close (only when this response ends the connection)
<empty line>
<body> (omitted for HEAD)
with CRLF everywhere. The server serves at most 4 requests per connection.
What you have not built
HTTPS is usually terminated ahead of you by a load balancer. HTTP/2 reuses all these semantics with binary framing over one multiplexed connection. A real deployment also needs timeouts, graceful shutdown on SIGTERM, and size limits on every read. They are the difference between "degrades under attack" and "falls over".
Your exercise
The starter reads the connection and loops over requests. You parse, route, build and decide.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…