HTTP/1.1 Server from Scratch
Welcome to the HTTP/1.1 Server from Scratch course!
In this course, you will build an HTTP/1.1 server entirely from scratch, strictly following the official RFC specifications (RFC 9110 and RFC 9112).
By the end of this course, you will understand TCP sockets, request parsing, routing, chunked encoding, concurrent connection handling and much more!
Tip
If you’ve never looked under the hood of HTTP before, it might sound intimidating. But don’t worry! You’ll quickly discover that at its core, HTTP is surprisingly simple and elegant. It’s mostly just reading and writing formatted text over a socket. You’ve got this!
The Rules of the Game
You can write your server in any programming language you want. However, to get the most out of this course, you must follow one rule: No HTTP libraries.
You may use your language’s standard library for network sockets (e.g., net in Node.js, std::net in Rust, socket in Python) and concurrency. You may NOT use frameworks (like Express, Django, or Actix) or standard library HTTP modules (like Go’s net/http).
Prerequisites
To complete this course, you will need:
- A programming language and runtime of your choice installed locally.
- Docker installed and running (to run the evaluation suite).
How the Evaluation Suite Works
As you progress through the stages, you will test your code using our official test runner.
The setup works like this:
- You run your custom server locally, binding it to
0.0.0.0on port8080. - You run our test suite via Docker.
- The Docker container fires a series of strict, edge-case HTTP requests at your server and evaluates the responses.
To run the entire evaluation suite against your server, open a separate terminal and run:
docker run \
--rm \
--add-host host.docker.internal:host-gateway \
codeberg.org/level0/buildit/http-server:latest \
--addr host.docker.internal:8080
What this command does:
--rm: Cleans up the container after the tests finish.--add-host host.docker.internal:host-gateway: Ensures the container can communicate with your local machine’s localhost.--addr: Tells the tester where to find your server.
Note
Instead of running the entire suite of tests every time, the runner uses an
--untilflag. This flag tells the tester to run all previous stages up to, and including, the stage you are currently working on. This ensures you haven’t broken any past functionality while building the new feature. At the end of every lesson, you will be provided with the exactdocker runcommand required to evaluate that specific stage.
Course Syllabus
The course is broken down into a series of gradually advancing stages. You must complete them in order, as each stage builds upon the last.
Ready to begin? Head over to the first stage: Accept a Connection.
Accept a Connection
Every HTTP request begins with a client connecting to a server over a transport layer. For HTTP, the standard transport layer is the Transmission Control Protocol (TCP).
TCP provides a reliable, ordered, and error-checked delivery of a stream of bytes between applications. Without TCP, HTTP wouldn’t know if the data it sent actually arrived!
Your Task
Bind a TCP listener to port 8080 on 0.0.0.0, wait for an incoming connection, and successfully accept it.
Warning
Why
0.0.0.0and notlocalhost? If you bind your server to127.0.0.1(localhost), it will only accept connections from inside your host machine. Because our test suite runs inside a Docker container, it connects to your server via a virtual network bridge. Binding to0.0.0.0ensures your server listens on all network interfaces, allowing the Docker container to successfully reach it.
Important
Run the below docker command to test your solution.
docker run \ --rm \ --add-host host.docker.internal:host-gateway \ codeberg.org/level0/buildit/http-server:latest \ --addr host.docker.internal:8080 \ --until accept_connection
Reference: RFC 9110 Section 3.3
Receive Data
Now that you have accepted a connection, you must read the HTTP Request being sent by the client.
HTTP is a text-based protocol. A client request is simply a stream of raw bytes containing ASCII text, formatted according to strict grammar rules defined in the RFCs.
The Anatomy of an HTTP Request
According to RFC 9112 Section 2.1, an HTTP message consists of:
HTTP-message = start-line
*( field-line CRLF )
CRLF
[ message-body ]
Let’s break down this ABNF (Augmented Backus-Naur Form) syntax:
start-line: A single line detailing the request (method, target, version).*( field-line CRLF ): Zero or more field lines (commonly known as headers), each ending with aCRLF. Semantically, these “field lines” carry the metadata of the HTTP message as key-value pairs. While developers usually call them “headers”, the specification formally refers to them as field lines because they are parsed line-by-line before being interpreted as logical fields.CRLF: An empty line signaling the end of the headers.[ message-body ]: An optional body payload.
Important
CRLFstands for Carriage Return (\r) and Line Feed (\n). You must look for\r\nto separate lines, not just\n. The end of the header section is explicitly marked by a doubleCRLFsequence (\r\n\r\n).
Your Task
The tester will connect to your server and slowly stream an HTTP request in multiple chunks.
You must read data from the connection until you encounter the \r\n\r\n sequence, which tells you
that the HTTP headers are finished.
For this stage, you don’t need to send a valid HTTP response back yet. However, your server
must not close the connection until it has fully received the \r\n\r\n boundary.
Warning: If your server uses a blocking read without checking for the \r\n\r\n boundary, it might hang indefinitely waiting for more data!
Important
Run the below docker command to test your solution.
docker run \ --rm \ --add-host host.docker.internal:host-gateway \ codeberg.org/level0/buildit/http-server:latest \ --addr host.docker.internal:8080 \ --until receive_data
Send a Response Line
You are successfully reading incoming requests. Now it’s time to send an HTTP Response back!
An HTTP Response, much like an HTTP Request, has a strict structure defined in RFC 9112 Section 4. It begins with a Status Line (often called a Response Line).
status-line = HTTP-version SP status-code SP reason-phrase CRLF
Example: HTTP/1.1 200 OK\r\n
HTTP-version: For this course, always useHTTP/1.1.SP: A single space character.status-code: A 3-digit integer indicating the result of the request (e.g.,200).reason-phrase: A short textual description of the status code (e.g.,OK).CRLF: The carriage return and line feed (\r\n).
Your Task
Read the incoming HTTP request (wait for the \r\n\r\n boundary).
Once received, regardless of what the request asked for, you must respond with a 200 OK status line.
Because you are not sending any headers or a body yet, your response must consist of exactly the status line, followed by the empty line that terminates the header section.
Your exact response must be:
HTTP/1.1 200 OK\r\n\r\n
Warning
Notice the two
\r\nsequences at the end! The first terminates the status line. The second acts as the empty line terminating the headers block. If you only send one\r\n, the client will hang indefinitely waiting for the headers to finish!
Important
Run the below docker command to test your solution.
docker run \ --rm \ --add-host host.docker.internal:host-gateway \ codeberg.org/level0/buildit/http-server:latest \ --addr host.docker.internal:8080 \ --until send_response_line
Parse the Request Line
Right now, you are blindly returning a 200 OK response to every connection. A real web server
routes requests based on what the client actually asked for!
To do this, you must parse the very first line of the incoming HTTP request, called the Request Line.
According to RFC 9112 Section 3, the Request Line has the following grammar:
request-line = method SP request-target SP HTTP-version CRLF
Example: GET /index.html HTTP/1.1\r\n
method: The action to be performed (e.g.,GET,POST). It tells the server what the client wants to do.request-target: Often called the URI or path (e.g.,/index.html), it tells the server which resource the action applies to.
Your Task
You must parse the Request Line of the incoming HTTP request and extract the request-target.
- If the request target is exactly
/, respond withHTTP/1.1 200 OK\r\n\r\n. - If the request target is anything else (e.g.,
/some/random/value), respond withHTTP/1.1 404 Not Found\r\n\r\n.
Tip
To parse the Request Line safely, read the incoming bytes until you find the first
\r\n. That entire string is the Request Line. You can then split it by the space character () to extract the method, target, and version.
Important
Run the below docker command to test your solution.
docker run \ --rm \ --add-host host.docker.internal:host-gateway \ codeberg.org/level0/buildit/http-server:latest \ --addr host.docker.internal:8080 \ --until parse_request_line
Parse Request Headers
After the Request Line, an HTTP request contains zero or more Fields (colloquially known as Headers).
Headers are metadata key-value pairs that provide additional context about the request (e.g., what kind of device the client is using, what data formats they accept).
According to RFC 9112 Section 5, these fields are transmitted as a series of field lines following this grammar:
field-line = field-name ":" OWS field-value OWS
Let’s break this down:
field-name: The name of the header (e.g.,User-Agent). Header names are case-insensitive. It meansUser-Agent,user-agent,USER-AGENT,uSeR-aGeNtare all the same.:: A literal colon character.OWS: Optional Whitespace. This means there might be zero spaces, one space, or multiple spaces before or after the value!field-value: The actual value of the header.
Example:
Host: localhost\r\n
user-agent: curl/7.68.0 \r\n
Caution
A very common pitfall is assuming there is always exactly one space after the colon. The RFC says
OWS(Optional Whitespace), meaningHost:localhostandHost: localhostare both perfectly valid and must be parsed identically. You must trim leading and trailing whitespace from the value!
Your Task
Create a new route: GET /ping.
When a client makes a request to this new endpoint, the tester will send requests containing a custom header: X-Test-Ping.
You must extract the value of this header for the /ping route only, taking care to handle case-insensitivity and whitespace trimming.
- If the request target is
/pingand the request contains the headerX-Test-Pingwith the exact valueping, respond with200 OK. - If the request target is
/pingand the header is missing, or has any other value, respond with400 Bad Request. - For other routes, maintain your existing logic (e.g.,
/returns200 OKregardless of headers).
Important
Run the below docker command to test your solution.
docker run \ --rm \ --add-host host.docker.internal:host-gateway \ codeberg.org/level0/buildit/http-server:latest \ --addr host.docker.internal:8080 \ --until headers_parsing
Validate Headers
Now that you can parse headers, let’s implement a strict rule from the HTTP/1.1 specification.
In the early days of the web (HTTP/1.0), a server assumed that any connection to its IP address was meant for the one and only website it hosted. But as the web grew, servers started hosting multiple websites on the same IP address (a technique called Virtual Hosting).
To make Virtual Hosting possible, HTTP/1.1 introduced the mandatory Host header. The client
uses this header to tell the server which website they are trying to reach.
The Strict RFC Requirement
According to RFC 9112 Section 3.2:
A client MUST send a Host header field in all HTTP/1.1 request messages. A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field.
This is not optional; it is a fundamental security and routing requirement for modern HTTP!
Your Task
Inspect the headers of the incoming request.
- If the
Hostheader is missing entirely, you MUST respond with400 Bad Request. - If the
Hostheader is present (with any value), respond with200 OK.
Important
Run the below docker command to test your solution.
docker run \ --rm \ --add-host host.docker.internal:host-gateway \ codeberg.org/level0/buildit/http-server:latest \ --addr host.docker.internal:8080 \ --until headers_validation
Send Response Headers
Just as clients send request headers to tell the server about themselves, servers send Response Headers to provide metadata about the response.
For example, if a server returns the string "hello", the client needs to know if that
string is plain text, a JSON object, or a piece of HTML. The server communicates this
using the Content-Type header.
Your Task
The tester will send a simple GET request. You must respond with a 200 OK status line,
and include a Content-Type: text/plain header.
Remember the grammar we learned for headers:
field-name ":" OWS field-value OWS CRLF
So, your response should look exactly like this:
HTTP/1.1 200 OK\r\n
Content-Type: text/plain\r\n
\r\n
Caution
Do not forget the empty
\r\nline at the very end of your response! According to RFC 9112 Section 2.1, the header section must be terminated by a doubleCRLF. Even if you only send one header, you still need that terminating blank line.
Reference: RFC 9110 Section 8.3
Important
Run the below docker command to test your solution.
docker run \ --rm \ --add-host host.docker.internal:host-gateway \ codeberg.org/level0/buildit/http-server:latest \ --addr host.docker.internal:8080 \ --until send_response_headers
Routing
Real HTTP servers host many different endpoints, and often, these endpoints include dynamic parameters in the URL path.
For example, if you visit a GitHub repository at https://github.com/torvalds/linux,
GitHub doesn’t have a static file for that exact path. Instead, they have a route
matching /github.com/{username}/{repo} and they extract torvalds and linux
from the URL dynamically!
Your Task
Set up a dynamic route: /echo/{string}.
When a client sends a GET request to /echo/something, you must extract the something
part of the path, and return it to the client.
Since you haven’t learned how to send Response Bodies yet, you will return the extracted
string inside a custom response header called X-Echo.
Example Request:
GET /echo/hello_world HTTP/1.1
Example Expected Response:
HTTP/1.1 200 OK\r\n
X-Echo: hello_world\r\n
\r\n
Tip
You can check if the request path starts with
/echo/and then extract the substring that follows it to get the dynamic parameter! Most languages provide string methods likestartsWith,substring,split, or slicing to make this easy. Just be sure to handle the case where the client requests a different path (continue returning404 Not Foundfor unknown paths).
Important
Run the below docker command to test your solution.
docker run \ --rm \ --add-host host.docker.internal:host-gateway \ codeberg.org/level0/buildit/http-server:latest \ --addr host.docker.internal:8080 \ --until routing
Send a Response Body
You’ve successfully routed requests and sent headers. Now let’s send actual data!
The Message Body is the payload of an HTTP message. It comes immediately after
the empty line (\r\n\r\n) that terminates the headers.
According to RFC 9110 Section 8.6,
when a server sends a message body, it MUST tell the client exactly how many bytes the body
contains using the Content-Length header.
If you omit this header, the client has no way to know when the response ends, and it will just hang there waiting for more data until the connection times out!
Your Task
Create a new route: /greet/{name}.
Extract the {name} from the URL, and return the greeting string Hello, {name}! as the Response Body.
You must also include a Content-Length header set to the exact length of the greeting string in bytes.
Example Request:
GET /greet/bob HTTP/1.1
Example Expected Response:
HTTP/1.1 200 OK\r\n
Content-Type: text/plain\r\n
Content-Length: 11\r\n
\r\n
Hello, bob!
Caution
The
Content-Lengthheader measures the number of bytes, not characters! For plain ASCII text this is the same, but if your server ever sends UTF-8 emojis,string.length()in many languages will return the wrong number. Always measure the byte array length!
Important
Run the below docker command to test your solution.
docker run \ --rm \ --add-host host.docker.internal:host-gateway \ codeberg.org/level0/buildit/http-server:latest \ --addr host.docker.internal:8080 \ --until response_body
Read Request Body
You know how to send a body, but what if the client sends one to you?
When a client sends data to the server (typically using a POST or PUT request),
they will include a Content-Length header in their request.
According to RFC 9112 Section 6, the length
of the message body is determined by the Content-Length header (unless Transfer-Encoding is present,
which we will cover later).
Your Task
Implement a new route: POST /uppercase.
When this route is called, you must:
- Parse the
Content-Lengthheader from the request. - After you find the
\r\n\r\nboundary separating headers and body, read exactlyContent-Lengthbytes from the socket. - Convert those bytes to uppercase.
- Send them back to the client as the response body (don’t forget your own
Content-Lengthheader in the response!).
Warning
A common mistake is reading the entire socket stream until EOF (End Of File). In HTTP/1.1, connections are kept alive! The client will not close the socket after sending the body. If you read until EOF, your server will hang forever. You MUST read exactly
Content-Lengthbytes and stop!
Important
Run the below docker command to test your solution.
docker run \ --rm \ --add-host host.docker.internal:host-gateway \ codeberg.org/level0/buildit/http-server:latest \ --addr host.docker.internal:8080 \ --until request_body
HTTP Methods
You are currently handling a POST request to /uppercase. But what if a client sends a
GET request to that same path?
Endpoints often only support specific HTTP Methods (GET, POST, PUT, DELETE, etc.).
According to RFC 9110 Section 9.3:
The method token indicates the request method to be performed on the target resource. The request method is the primary source of semantics for a request message.
If a client attempts to use a method that the server does not support for a specific resource,
the server should respond with a 405 Method Not Allowed status code.
Your Task
Protect your /uppercase route!
- If it receives a
POSTrequest, function normally (read the body, uppercase it, return it). - If it receives any other method (like
GET), respond immediately with405 Method Not Allowed(and no response body).
Important
Run the below docker command to test your solution.
docker run \ --rm \ --add-host host.docker.internal:host-gateway \ codeberg.org/level0/buildit/http-server:latest \ --addr host.docker.internal:8080 \ --until http_methods
Concurrent Connections
Up until now, we’ve only sent one request at a time. But a real HTTP server must handle multiple clients simultaneously!
Imagine a slow client connects to your server and starts sending a request, but pauses halfway through. If your server processes connections synchronously (blocking on one client until they finish before accepting the next), that one slow client just froze your entire server!
To handle real-world traffic, your server must be able to juggle multiple connections at once.
Your Task
Ensure your server can handle concurrent connections. You can achieve this using multi-threading
(e.g., spawning a new thread per accepted socket connection) or asynchronous I/O (e.g., using
epoll/kqueue or an async runtime like Tokio or Node.js).
How we test this: The tester will open two connections at the exact same time. It will send a slow, incomplete request on Connection 1. While Connection 1 is sitting idle waiting for more data, the tester will send a complete, fast request on Connection 2.
Your server must respond to Connection 2 immediately, without waiting for Connection 1 to finish!
Important
Run the below docker command to test your solution.
docker run \ --rm \ --add-host host.docker.internal:host-gateway \ codeberg.org/level0/buildit/http-server:latest \ --addr host.docker.internal:8080 \ --until concurrent_connections
Chunked Encoding (Read)
You know how to read a request body using Content-Length. But what if a client wants
to stream data to you, and they don’t know the final size before they start sending?
In HTTP/1.1, this is solved using Chunked Transfer Encoding (RFC 9112 Section 7.1).
When a request contains the header Transfer-Encoding: chunked, the Content-Length header is omitted.
Instead, the body is sent in a series of chunks.
Each chunk follows this strict ABNF grammar:
chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF
chunk-size = 1*HEXDIG
chunk-size: A hexadecimal number indicating the size of the following chunk data.[ chunk-ext ]: Optional extensions that you can safely ignore for this course.chunk-data: The actual payload bytes of the specified size.
Example of a chunked body:
4\r\n (Chunk size: 4 bytes in hex)
Wiki\r\n (Chunk data: 4 bytes)
5\r\n (Chunk size: 5 bytes in hex)
pedia\r\n (Chunk data: 5 bytes)
0\r\n (Final zero-length chunk terminating the stream)
\r\n (End of chunked body)
Your Task
Create a new /lowercase route to support Transfer-Encoding: chunked.
If the client sends a chunked request, you must parse the chunk sizes (hexadecimal!),
read exactly that many bytes of chunk data, skip the \r\n, and repeat until you encounter
a chunk of size 0.
Finally, combine all the chunk data, lowercase it, and return it in the response (you can
return it with a standard Content-Length response for now).
Important
Run the below docker command to test your solution.
docker run \ --rm \ --add-host host.docker.internal:host-gateway \ codeberg.org/level0/buildit/http-server:latest \ --addr host.docker.internal:8080 \ --until chunked_encoding_read
Chunked Encoding (Write)
Servers also need to send chunked responses when they generate data dynamically,
or stream large files, and don’t know the final Content-Length in advance.
Imagine you are streaming a video, or returning rows from a slow database query. Instead of waiting 10 seconds to calculate the total length and buffering it all in memory, you can send the data in chunks as soon as it’s ready!
Your Task
Create a new route: GET /stream.
When a client requests this endpoint, respond with Transfer-Encoding: chunked (do NOT
include a Content-Length header).
You must send the body in at least two separate chunks, followed by the zero-length
terminating chunk (0\r\n\r\n).
The combined payload of your chunks must EXACTLY match this quote by Tim Berners-Lee: The Web is more a social creation than a technical one.
Tip
Make sure you format the chunk sizes as Hexadecimal strings!
10bytes should bea\r\n, not10\r\n.
Reference: RFC 9112 Section 7.1
Important
Run the below docker command to test your solution.
docker run \ --rm \ --add-host host.docker.internal:host-gateway \ codeberg.org/level0/buildit/http-server:latest \ --addr host.docker.internal:8080 \ --until chunked_encoding_write
Keep-Alive Connections
In HTTP/1.0, a new TCP connection was opened for every single request. The client would send a request, the server would send a response, and then the server would immediately close the TCP connection.
This was incredibly slow because TCP requires a 3-way handshake to establish a connection. If a webpage needed 50 images, it required 50 separate handshakes!
HTTP/1.1 introduced Persistent Connections (often called Keep-Alive) to solve this.
According to RFC 9112 Section 9.3:
HTTP/1.1 defaults to the use of “persistent connections”, allowing multiple requests and responses to be carried over a single connection.
By default, a server must NOT close the connection after sending a response. It should keep the connection open and wait for the client to send another request.
What about Connection: keep-alive?
You might have seen headers like Connection: keep-alive or Keep-Alive: timeout=5 in the wild.
In the older HTTP/1.0 protocol, connections were closed by default. To keep a connection open, clients had to explicitly send a Connection: keep-alive header.
However, in HTTP/1.1, persistent connections are the default. This means Connection: keep-alive is technically redundant (though many browsers still send it for backwards compatibility). Unless the client or server explicitly sends a Connection: close header, you should assume the connection remains open.
The Keep-Alive header (which provides timeout hints) is also an older extension and is not strictly required by the modern HTTP/1.1 spec. You do not need to parse or send these headers for this stage!
Your Task
Ensure your server keeps the connection open after sending a response.
The tester will open a single connection and send two separate HTTP requests sequentially over that same connection. Your server must respond to both of them.
Tip
You can implement this by wrapping your connection handling logic in a continuous loop! Just be sure to detect when the client closes the connection (typically when reading from the socket returns 0 bytes, or an EOF signal is received). Once that happens, you should safely break out of the loop and clean up.
Important
Run the below docker command to test your solution.
docker run \ --rm \ --add-host host.docker.internal:host-gateway \ codeberg.org/level0/buildit/http-server:latest \ --addr host.docker.internal:8080 \ --until keep_alive
File Serving
You have built an HTTP server capable of dynamic routing, concurrent processing, and chunked encoding! Now, let’s go back to the roots of the web: serving static files.
When a client requests an image, an HTML file, or a stylesheet, the server must read the file from the filesystem and return its contents in the Response Body.
Your Task
Because the tester runs inside a Docker container and your server runs on your host machine, they need a shared directory to test file serving. The tester will create a temporary file in this directory, and then ask your server to serve it!
Configure your server to serve files from a directory of your choice.
You must implement a new route: GET /files/{filename}.
When a client requests this endpoint, your server must:
- Combine your chosen base directory with the
{filename}from the URL. - Open the file on disk.
- If the file exists, return a
200 OKresponse with:
Content-Type: application/octet-streamContent-Lengthset to the size of the file.- The file contents as the Response Body.
- If the file does NOT exist, return a
404 Not Foundresponse.
Note
application/octet-streamis the standard MIME type for an arbitrary binary file.
Important
Run the below docker command to test your solution. Note the
-vflag: you must replace<YOUR_DIRECTORY>with the absolute path to the directory your server is serving from!This mounts your host directory to
/file_servinginside the container. The tester is hardcoded to create and request test files from this specific/file_servingpath!docker run \ --rm \ -v <YOUR_DIRECTORY>:/file_serving \ --add-host host.docker.internal:host-gateway \ codeberg.org/level0/buildit/http-server:latest \ --addr host.docker.internal:8080 \ --until file_serving
Compression
Bandwidth is expensive, and plain text formats (like HTML, CSS, and JSON) are highly compressible. To make the web faster, HTTP supports Content Encoding.
When a client makes a request, they can include the Accept-Encoding header to tell the
server which compression algorithms they understand (e.g., gzip, deflate, br).
If the server supports one of those algorithms, it can compress the Response Body, and add
a Content-Encoding header to the response to tell the client how to decompress it!
Your Task
Create a new route: /compress/{string}.
- Read the
Accept-Encodingheader from the request. - If it contains
gzip(it might be a comma-separated list likegzip, deflate), you must:
- Compress the
{string}payload using the GZIP algorithm. - Add the header
Content-Encoding: gzip. - Send the compressed bytes as the response body.
- Set
Content-Lengthto the size of the compressed payload.
- If
Accept-Encodingdoes not containgzip, or is missing entirely, respond exactly as you did in Send a Response Body (uncompressed plain text).
Note
You will need a GZIP library in your language to compress the string! (e.g.
flate2in Rust,zlibin Node.js/Python,compress/gzipin Go).
Important
Run the below docker command to test your solution.
docker run \ --rm \ --add-host host.docker.internal:host-gateway \ codeberg.org/level0/buildit/http-server:latest \ --addr host.docker.internal:8080 \ --until compression
🎉 Congratulations! 🎉
You have successfully built a working HTTP/1.1 server entirely from scratch!
You started with a raw TCP socket and progressively implemented the intricate details of the HTTP protocol. By the end, your server was capable of dynamic routing, concurrent connection handling, chunked transfer encoding, persistent connections, file serving, and even gzip compression!
Building foundational software like an HTTP server from scratch is one of the most effective ways to truly understand how the systems we rely on every day actually work under the hood.
What We Covered
Throughout this course, you tackled a massive slice of HTTP/1.1:
- TCP Sockets: Binding, listening, and reading/writing raw bytes.
- Protocol Parsing: Extracting the request line, parsing headers, and reading request bodies.
- Routing & Methods: Handling
GETandPOSTrequests, and routing them to different endpoints. - Concurrency: Using threads or asynchronous tasks to handle multiple clients simultaneously without blocking.
- Chunked Transfer Encoding: Implementing
Transfer-Encoding: chunkedto stream unknown-length payloads. - Persistent Connections: Implementing
Keep-Aliveto reuse TCP connections. - File Serving: Serving static files from disk with the correct MIME types.
- Compression: Using GZIP via the
Content-Encodingheader to reduce bandwidth.
What We Didn’t Cover
HTTP is a massive, continuously evolving protocol. To keep this course focused, we intentionally omitted some advanced features:
- Query Parameters: Parsing the
?key=valuestring from the URL path. - HTTPS / TLS: We only implemented plain-text HTTP. Adding TLS requires integrating cryptographic libraries and performing TLS handshakes over the TCP socket.
- HTTP/2 and HTTP/3: These are fundamentally different protocols. HTTP/2 uses multiplexed binary framing over TCP, and HTTP/3 operates over UDP using QUIC.
- Advanced Caching: ETags,
Cache-Control,Last-Modified, and conditional requests (e.g.,If-None-Match). - Cookies and Sessions: Setting
Set-Cookieheaders and managing server-side state. - WebSockets: The
Upgradeheader mechanism to turn an HTTP connection into a full-duplex WebSocket stream.
What’s Next?
If you enjoyed this, here are a few ideas for what you can do next:
- Explore HTTP/2: Read the RFCs for HTTP/2 to see how the protocol evolved to solve performance bottlenecks. It uses multiplexed binary framing over TCP, which is a completely different beast!
- Explore a Real Server: Look at the source code of a production-ready HTTP server in your language (like
Hyperin Rust ornet/httpin Go). You’ll recognize many of the concepts you just built, and you can see how they handle edge cases and performance optimizations! - Try another
builditcourse: If you enjoyed building an HTTP server from scratch, check out the other courses in thebuilditproject! Discover what other foundational technologies you can demystify by building them from the ground up.
Thanks for taking this journey with us. Now go out there and build something awesome!