Skip to content

Golang HTTP Server (part 2)

Posted on:26 gennaio 2025Β atΒ 19:50

This is a continuation of the work done in part 1, in this post we will introduce a new http parser and a new http request type to better handler the incoming request.

HTTP Request

In the last chapter we left ourselves with a first version of the server in which they are handled as strings and passed through the various functions. This part needed a more suitable structure for better maintenance, so in the http package is defined a HttpRequest struct which defines the fields of a request.

type HttpRequest struct {
  Method  string
  Url     string
  Headers map[string]string
  Body    string
  Query   string
}

HTTP Parser

To transform incoming request from

GET /echo/hello HTTP/1.1
Host: localhost:3000
User-Agent: curl/8.1.2
Accept: */*
Content-Type: application/json
Content-Length: 22

{
  hello: "world"
}

to a struct like the one defined before we need a parser.

The HTTP standard suggest to read the start-line into a structure, read each header field line into a hash table by field name until the empty line, and then use the parsed data to determine if a message body is expected. If a message body has been indicated, then it is read as a stream until an amount of octets equal to the message body length is read or the connection is closed.

A possibile implementation is defined in http package:

func HttpParser(rawRequest string) (HttpRequest, error) {
	var request HttpRequest

	if len(rawRequest) == 0 {
		return HttpRequest{}, errors.New("empty http request")
	}

	if request.Headers == nil {
		request.Headers = make(map[string]string)
	}

	scanner := bufio.NewScanner(strings.NewReader(rawRequest))
	scanner.Scan()

	line := strings.Split(scanner.Text(), " ")

	request.Method = line[0]
	request.Url = line[1]

	for scanner.Scan() {
		line := scanner.Text()

		if line == "" {
			break
		}

		header := strings.Split(line, ":")
		request.Headers[header[0]] = header[1]
	}

	for scanner.Scan() {
		request.Body += scanner.Text()
	}

	return request, nil
}

The function checks if the raw request is empty, returns an error if so, then initializes an empty headers map if not already created.

Uses bufio.NewScanner to read the request line by line splitting the first line to extract HTTP method and URL and set it on request.Method and request.Url

After the first line scans through subsequent lines until an empty line, and for each line splits each header line by ”:” and stores headers in request.Headers map.

After headers, continues scanning to collect request body.

Conclusion

Now we have a functional http parser for converting raw HTTP request string into a structured HttpRequest object.

In the next steps, I would like to delve deeper into what happens if the requests exceed 1024 bytes.

req := make([]byte, 1024)
n, err := con.Read(req)

Currently, the server handles each request over a fixed length of 1024 bytes, but what happens if the request exceeds that size?