Skip to content
Lesson 12 of 16

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

  1. Request line (lesson 1): three fields, shape rules, then 501 for an unknown method and 505 for an unsupported version.
  2. Headers (lesson 2): name: value lines up to the empty line, lowercased names, trimmed values. A malformed line is 400. HTTP/1.1 needs exactly one Host.
  3. Body framing (lesson 3): Content-Length or chunked, never both. A bad length or bad chunk is 400. If the bytes run out before the message is complete, the client is gone: there is nothing to answer, so stop.
  4. Route (lessons 5 and 6): pick the handler, or 404, or 405 with Allow.
  5. Response (lesson 4): build status line, headers, Content-Length, body. HEAD gets the headers and no body.
  6. 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.

RequestResponse
GET /, HEAD /200, body Welcome to tinyhttp and a newline
GET /user-agent200, the User-Agent value (empty if absent)
GET /echo/<text>200, the percent-decoded segment <text> (non-empty, bad escape is 400)
POST /echo200, 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.

Up nextConditional Requests & ETagsCaching, Compression & Beyond

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…