Making a HTTP server in Go
Tags: go, codenewbie, codecrafters, backend
Published: 2024-07-15 12:44:57

I've stumbled upon [Codecrafters](https://codecrafters.io/) a while ago, and the idea really caught my attention. Making real world projects to learn, instead of following tutorials is everything I could've asked for when I was getting started, so just so I don't let the opportunity get away, I'm following their free project on writing a [HTTP server](https://app.codecrafters.io/courses/http-server/).
I'm choosing Go for this project, I've been meaning to learn it fully for a good while now

## Starting the project
Codecrafters has one of the coolest features I've seen for validating tests, once you commit your code to their repo, tests are automatically ran and your progress is validated. So, for this first step, all we have to do is un-comment some of the provided code to create our TCP server

```go
import (
	"fmt"
	"net"
	"os"
)

func main() {
	fmt.Println("Logs from your program will appear here!")
	l, err := net.Listen("tcp", "0.0.0.0:4221")
	if err != nil {
		fmt.Println("Failed to bind to port 4221")
		os.Exit(1)
	}
	
	_, err = l.Accept()
	if err != nil {
		fmt.Println("Error accepting connection: ", err.Error())
		os.Exit(1)
	}
}

```
And everything goes smoothly!

![Terminal Screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ay13elreilrmok17qzux.png)

For this second step, we're returning a response from our server.
Codecrafters has this really cool "Task" card that explains everything you need to know to implement the next step

![Codecrafters screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jcwrnracsok7dzb39a0c.png)

So, we add a variable for our connected client and send him a Status 200
```go
	//added conn for handling responses
	con, err := l.Accept()
	if err != nil {
		fmt.Println("Error accepting connection: ", err.Error())
		os.Exit(1)
	}

	//variables for returning response
	var success string = "HTTP/1.1 200 OK\r\n"
	var ending string = "\r\n"

	//write into the connection
	con.Write([]byte(success + ending))
```

##Extracting URL Paths
Now, we need to give a proper response based on the path that is being accessed on our server.
We can identify the path using the received request
``` HTTP/1.1 200 OK ```

And since we're expanding the project scope, I've decided to create a utility package for handling operations like parsing and returning responses

``` /utils/handler.go ```
``` go
package utils

import "net"

type Handler struct {
	Conn net.Conn
}

func (h *Handler) Success() {
	//variables for returning response
	var success string = "HTTP/1.1 200 OK\r\n"
	var ending string = "\r\n"

	//write into the connection
	h.Conn.Write([]byte(success + ending))
}

func (h *Handler) NotFound() {
	//variables for returning response
	var notFound string = "HTTP/1.1 404 Not Found\r\n"
	var ending string = "\r\n"

	//write into the connection
	h.Conn.Write([]byte(notFound + ending))

}

func (h *Handler) Parse(req []byte) {
	h.Conn.Read(req)
}

```

And in the main file , we add our connection to our handler and check the path of the request to return our responses
``` server.go ```

```go
	//initialize the handler
	handler := utils.Handler{Conn: con}
	req := make([]byte, 1024)

	//parses the request to a readable format
	handler.Parse(req)

	fmt.Println("Request: ", string(req))

	//path checking
	if !strings.HasPrefix(string(req), "GET / HTTP/1.1") {
		handler.NotFound()
		return
	}
	handler.Success()
	return
```

##Returning a response
Once again, I feel like I need to refactor my code, not only for adding some essentials I've forgotten but also to practice my Clean Code 😆
In the main file, we add a function to handle incoming connections
``` server.go ```
```go 
func main() {
	fmt.Println("Starting server on port 4221")

	l, err := net.Listen("tcp", "0.0.0.0:4221")
	if err != nil {
		fmt.Println("Failed to bind to port 4221")
		os.Exit(1)
	}

	//if handled thread is closed / finished, close the listener
	defer l.Close()

	//keep listening to requests
	for {
		con, err := l.Accept()
		if err != nil {
			fmt.Println("Error accepting connection: ", err.Error())
			os.Exit(1)
		}
		go handleConnection(con)
	}
}

func handleConnection(con net.Conn) {
	//if returned, close connection
	defer con.Close()

	//initialize the handler
	handler := utils.Handler{Conn: con}
	req := make([]byte, 1024)

	//parse the request
	urlPath := handler.GetURLPath(req)
	fmt.Println("URL Path: ", urlPath)

	if urlPath == "/" {
		handler.Success()
		return
	}
	handler.NotFound()
	return
}

```

And in the handler we've written in the previous step, we add a function to get the URL path of a incoming request
``` /utils/handler.go ```
```go 
func (h *Handler) GetURLPath(req []byte) string {
	parsed := h.Parse(req)
	firstheader := strings.Split(parsed[0], " ")
	return firstheader[1]
}
```

For this step, we need to implement the /echo/:message endpoint, for returning whatever message is sent through the URL to the response body, for now, we can add a simple if statement checking the path
``` /app/server.go ```
``` go
	//parse the request
	urlPath := handler.GetURLPath(req)
	fmt.Println("URL Path: ", urlPath)

	if urlPath == "/" {
		handler.Success()
		return
	}else if strings.Split(urlPath, "/")[1] == "echo" {
		msg := strings.Split(urlPath, "/")[2]
		handler.SendBody(msg)
		return
	}
	handler.NotFound()
	return
``` 

##Reading the User-Agent header
In this step we need to get the data from the 'User-Agent' header and return it in the body of our response
First of all, we add a route to our server, and while we're at it, we can refactor the 'router' to something a little more readable

``` server.go ```
```go
	switch strings.Split(urlPath, "/")[1] {
	case "echo":
		msg := strings.Split(urlPath, "/")[2]
		handler.SendBody(msg, len(msg))
		return
	case "user-agent":
// not the prettiest code ever, but hey, it works
		d := handler.UserAgent(req)
		d_msg := strings.Split(d, ": ")[1]
		d_len := len(d_msg)
		fmt.Println(d_msg)
		fmt.Println(d_len)
		handler.SendBody(d_msg, d_len)
		return
	default:
		handler.NotFound()
	}
```
And we add a new function in our handler to get the needed header value
``` /utils/handler.go ```
```go
// UserAgent returns the User-Agent header from the request.
func (h *Handler) UserAgent(req []byte) string {
	// dont need to read the connection again
	parsed := strings.Split(string(req), "\r\n")
	for i := range parsed {
		if strings.Contains(parsed[i], "User-Agent") {
			return parsed[i]
		}
	}
	return ""
}
}
```
We also update our _SendBody_ function to get the correct content-length based on a parameter
```go
func (h *Handler) SendBody(message string) {
	//variables for returning response
	var success string = "HTTP/1.1 200 OK\r\n"
	var contentType string = "Content-Type: text/plain\r\n"
	var contentLength string = fmt.Sprintf("Content-Length: %d\r\n", len(message))
	var ending string = "\r\n"

	returnBody := success + contentType + contentLength + ending + message

	//write into the connection
	h.Conn.Write([]byte(returnBody))
}
}
```


## Handling concurency
The next step would be handling multiple connections at the same time, our code already does that! In our main function, we call the _handleConnection_ function using the **go** keyword, creating a **goroutine** for each connection
``` server.go ```
```go

	//if handled thread is closed / finished, close the listener
	defer l.Close()

	//keep listening to requests
	for {
		con, err := l.Accept()
		if err != nil {
			fmt.Println("Error accepting connection: ", err.Error())
			os.Exit(1)
		}
		go handleConnection(con)
	}
}
```
## Sending Files 
In this activity, we implement the _/files/:file_ endpoint, this route returns a file in the body of the response to the user (if the file is found)
For achieving this, we write a function in our _/utils/handler.go_ for sending the file
``` /utils/handler.go ```
```go
func (h *Handler) SendFile(data []byte) {
	//variables for returning response
	var success string = "HTTP/1.1 200 OK\r\n"
        // make sure to get the correct content-type
	var contentType string = "Content-Type: application/octet-stream\r\n"
	var contentLength string = fmt.Sprintf("Content-Length: %v\r\n", len(data))
	var ending string = "\r\n"
	returnBody := success + contentType + contentLength + ending + string(data)
	//write into the connection
	h.Conn.Write([]byte(returnBody))

}
```
And in our main server file, we add a route to our switch statement
``` server.go ```
```go
	case "files":
		filename := strings.Split(urlPath, "/")[2]
		//get the folder to the file
                filepath := os.Args[2]
		fmt.Printf("DIR: %s\n", filepath)
                //create folder if it dosent exist
		if _, err := os.ReadDir(filepath); err != nil {
			fmt.Println("Creating directory...")
			os.Mkdir(filepath, 0755)
		}
		fmt.Println("Reading: ", filepath+filename)
		data, err := os.ReadFile(filepath + filename)
		if err != nil {
			fmt.Println("Error on reading file")
			handler.NotFound()
			return
		}
		fmt.Println("Sending file...")
		handler.SendFile(data)
```

## Reading the request body
Our last challenge is reading the body of a post request
Using our _/files/:fileName_ endpoint, we should read a POST request, create a file with the file name provided in the URL. In this created file we add the data provided in the post request
For this to be achieved, we add a function to our _/utils/handler.go_ file to get the contents of the request body, method and a modification to our _SendBody_ function
``` /utils/handler.go ```
```go
func (h *Handler) GetBody(req []byte) string {
	parsed := h.Parse(req)

	return parsed[len(parsed)-1]
}

func (h *Handler) GetMethod(req []byte) string {
	parsed := h.Parse(req)
	firstheader := strings.Split(parsed[0], " ")
	return firstheader[0]
}

func (h *Handler) SendBody(message string, msgLen int, stcode int) {
	//variables for returning response
	var success string = fmt.Sprintf("HTTP/1.1 %v %v\r\n", stcode, http.StatusText(stcode))
	var contentType string = "Content-Type: text/plain\r\n"
	var contentLength string = fmt.Sprintf("Content-Length: %v\r\n", msgLen)
	var ending string = "\r\n"

	returnBody := success + contentType + contentLength + ending + message

	//write into the connection
	h.Conn.Write([]byte(returnBody))
}

```
And that's it!! We've made an HTTP server from (almost) scratch, using nothing but the std library in Golang

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9fjcofh268v9vdgh8iqo.png)


I'll definitely make more of those Codecrafters challenges in the future, so stay tuned to see!