An Implementation of the HTTP Protocol.
March 10th, 2026 — Programming
TLDR: An HTTP server is an implementation of the HTTP protocol
What is HTTP?
The Hyper Text Transfer Protocol (HTTP) enables communication on the web. As the name implies, it works through using raw text to convey information about a “request” and the corresponding “response” to that request. It works off the client-server model where an agent/user/person is acting as the client who is requesting some type of information from a server, which is just a transmission medium. Essentially, HTTP allows data to travel between networks using text that us humans can understand.
HTTP is an application layer protocol. It builds on top of the previous layers in the OSI models. For example, you can implement HTTP over TCP or over UDP. However, a key detail that helped me understand HTTP in a deeper manner was the fact that the core semantics of the protocol is the same across all versions of HTTP. Properties like the message structure, methods and status code don’t change. For more information on this, reference RFC 9110 here: HTTPs://www.rfc-editor.org/rfc/rfc9110.html#name-core-semantics
One of the key facts about HTTP is it’s stateless. This means that in the implementation of HTTP, no information about the transactions between the client and server should be stored. No details about the state or history of the communication between the two parties are to be present.
The next few parts will breakdown the HTTP protocol by the order in which it happens. Then, I’ll go in detail as how an HTTP server is created. This may sound confusing because one could assume that an HTTP server is synonymous to the HTTP protocol. However, the important distinction between the two is that the HTTP protocol follows the specified guidelines in the RFC’s while an HTTP server is an implementation of the protocol itself. In short, you can’t have an HTTP server without the HTTP protocol.
For context, I used the 1.1 version of the protocol. I followed the course on boot.dev for the actual protocol implementation but I strayed away from it when building the logic for the corresponding server to learn more about how handlers and multiplexers work underneath.
The HTTP Protocol
How data is received in HTTP:
The way data flows in an HTTP server is similar to messaging between two people. Data (messages) is sent back and forth. Once a request is sent by the user, the server then sends back a message to it via a response. To be more specific, the basic unit of communication in the protocol is called an HTTP message. This contains all of the data that allows for our protocol to work. The details of an HTTP message are broken down into three parts:
- Start line: In the beginning line of an HTTP message, we get information about what type of request is being sent by the client to our server, the resource path, and the HTTP version.
- Header(s) Lines: After the beginning of the first line, we get into the headers, which are made up of field lines. Each field line is composed of a field name and the corresponding value.
- Body: Lastly, the body follows the last field line. This just carries information.
References for field syntax(headers) parsing: RFC 9110 section 5.5 and 9112
After each section, a delimiter has to be included so that each section has a boundary. The protocol uses the Carriage Return + Line Feed (\r\n) for this purpose. It counts as two characters, which is a very important fact to remember! Counting the CRLF as one character is going to cause tons of headaches in later parsing functions, so hang onto that information.
The Request Line
As stated before, the request line carries specific information about the … request. There is a specific format which we have to follow:
<Method> SP <Resource> SP <HTTP version> CRLF An incorrect spacing or no carriage return line feed will cause an error, so it’s imperative to abide by these formatting standards. An invalid request type or HTTP version will also throw an error but those are more of semantic issues rather than formatting problems.
The Headers
Similar to the request line, the headers must follow the format set by RFC 9112:
<Field Name>: OWS <Field Value> OWS Note that there should be no trailing space before the colon or field name.
Since commas are used as a delimiter to separate field line values (such as an accepting field line with the corresponding acceptance formats), its important to be careful that our parser(s) allow for that. We don’t want to accidentally omit parts of the field values by just splitting on the spaces:
Accept: text/html, application/xhtml+xml, image/webp result of splitting on spaces:
["Accept:", "text/html,", "application/xhtml+xml,", "image/webp"] Field values are constrained to US-ASCII characters and should be case insensitive. Another important field value constraint is in RFC 9112 section 6.1, which states that “a server MAY reject a request that contains both Content-Length and Transfer-Encoding or process such a request in accordance with the Transfer-Encoding alone.” In my implementation, we will not allow for both headers to be defined in a request. So, a valid request must have either a transfer encoding or content length header or neither. Neither would signify that there is no message body.
The Body
The body of an HTTP message is optional for some request methods that don’t rely on some type of data being carried to our server. For example, a POST request will be required to have a body because its main purpose is to update a resource which requires data to accomplish that action. This is also entirely dependent on if the HTTP message has a transfer encoding or content length since the value of those fields contains information about the body.
When a content length field is present, we have a straightforward approach in how many bytes our parser should read up to. Any information beyond the anticipated size does not get read.
Content-Length: 10\r\n\r\ntesting te The transfer encoding field case is a bit more complex. Each different type of transfer encoding is handled differently. For example, when the transfer encoding has a value of chunked, each “chunked” part of the body has a prefix of its size in hexadecimal. A value of 0 indicates the end of the body.
Transfer-Encoding: chunked\r\n\r\n7\r\nWelcome\r\n1c\r\ntesttest test test test test\r\n0\r\n\r\n The HTTP server implementation
First Steps
In the HTTP protocol, most of the internal work is done by the request parser. However, there is one more crucial part. We must serve the client the information requested accurately. Now, we move a tad bit from the abstraction of the protocol, meaning that the method in which we design the server is mostly up to us. I will be largely taking ideas from the net/HTTP package in golang to get a deeper level of understanding and maybe even spot areas of improvement.
Data can be sent in small packets or chunks. It won’t always be the case that all of the request will be sent at once. So, the first step in our implementation of an HTTP server would be to find a common delimiter to know when each part of the HTTP message is done being sent. As mentioned before, we will use the carriage return line feed.
Parsing
As mentioned previously, the parser in our server allows for the extraction of information from an incoming valid HTTP message. Each individual part of the message should be parsed separately. One way to do this is by using a state machine that keeps track of the position of the parser relative to where in the HTTP message it’s at. Below is an example of the state machine in my implementation:
StateInit → StateHeaders → StateBody → StateDone
↓
StateBodyFixed / StateBodyChunkedRead and StateBodyChunkedWrite Once the parser arrives at the StateDone state, the server moves onto generating the response.
Multiplexers
In an HTTP server, a multiplexer allows for different routes or paths to be configured to a handler. It’s also known as a mux or router. If no multiplexer is defined in an HTTP server, the default multiplexer is used. However, it’s always better to define a multiplexer since you would be forced to write a bunch of if/else statements to check the resource path without it. The actual implementation of it uses a hashmap, where the key is a structure containing the information about the method and route while the value is an instance of the handler interface.
Multiplexer:
type routeKey struct {
method string
path string
}
type MyServerMux struct {
routes map[routeKey]handler.Handler
}
Handler Interface:
type Handler interface {
ServeHttp(response.ResponseWriter, *request.Request)
} To use the multiplexer, you would have to register a route with its handler using it’s HandleFunc method.
Handlers
To avoid confusion, when referencing a handler, I define it as a method of responding to an HTTP request by using some type of response writer. A response writer is simply an interface that is implemented to write a response back to a client. It’s self explanatory.
The purpose of handlers is to take context of an HTTP message and the parameters provided by the multiplexer to execute the logic for a response. Once the work of the business logic is done, the response writer has to send back the status code as well as the data payload in the response back to the client.
mux.HandleFunc("GET", "/", func(w ResponseWriter, r *Request) {
w.CustomWriteHeader(200)
w.Write([]byte("Hello, World!"))
}) Here, a handler is registered that just sets the status of the response to 200 and the body to “Hello, World!”
Server Logic
When actually starting up an HTTP server, we need to open up a tcp connection and listen for any incoming requests by using a for loop with termination signals to close the connection once it’s not needed anymore. This behavior is wrapped inside a function called ListenAndServe. In order to handle multiple concurrent requests inside the ListenAndServeFunction, we create a go routine with an incoming request as a parameter and serve that request inside the go routine.
go func(c net.Conn) {
err := serve(c, h)
if err != nil {
println("Error in trying to serve connection", err.Error())
}
}(conn) This is essentially what happens when you start up an HTTP server. Very simple but there is a ton of underlying functions working in conjunction to satisfy the guidelines of HTTP.
Conclusion
Most of the web frameworks hide the abstraction of HTTP, making it easy to overlook. Understanding how each part of HTTP works in your application puts you at a massive advantage when debugging complex issues as well as giving you the ability to build efficient systems.
Checkout my full implementation here