We're sorry but this app doesn't work properly without JavaScript enabled. Please enable it to continue.

This lesson's interactive features are locked, please to keep using them

Parsing the Request Line

Now it's time for the best part of any project, the string parsing! By building on top of TCP, we already have code that handles plain-text data, now we just need to take that plain text and turn it into structured data, ensuring that it follows the HTTP protocol.

For example, given:

POST /coffee HTTP/1.1
Host: localhost:42069
User-Agent: curl/7.81.0
Accept: */*
Content-Length: 21

{"flavor":"dark mode"}

We want our HTTP parser to return a struct that looks like this:

type Request struct {
    RequestLine RequestLine
    Headers     map[string]string
    Body        []byte
}

That way our application logic (the server code) has nicely structured HTTP data to work with.

Our goal is to take the server we created in the last section and have it parse out the start-line according to the RFC message parsing section.

The Request-Line

Remember how HTTP messages start with a start-line? Well, if it's a request (not a response), then the start-line is called the request-line and has a specific format.

HTTP-version  = HTTP-name "/" DIGIT "." DIGIT
HTTP-name     = %s"HTTP"
request-line  = method SP request-target SP HTTP-version

Which is a bit hairy to read (because its accounting for all the possible options), but for HTTP/1.1 it's pretty simple, an example request-line looks like this:

GET /coffee HTTP/1.1

Assignment

For now, we're only going to parse the request-line.

Here we are, using test-driven-development, Uncle Bob would be proud.

On a real note, when it comes to blackbox RFCs like this, good tests help us move (blazingly) faster and avoid bugs.

  1. type Request struct {
        RequestLine RequestLine
    }
    
    type RequestLine struct {
        HttpVersion   string
        RequestTarget string
        Method        string
    }
    

    Don't forget to capitalize your exports smh...

    Hey, it's still better than public static void main...

  2. // Test: Good GET Request line
    r, err := RequestFromReader(strings.NewReader("GET / HTTP/1.1\r\nHost: localhost:42069\r\nUser-Agent: curl/7.81.0\r\nAccept: */*\r\n\r\n"))
    require.NoError(t, err)
    require.NotNil(t, r)
    assert.Equal(t, "GET", r.RequestLine.Method)
    assert.Equal(t, "/", r.RequestLine.RequestTarget)
    assert.Equal(t, "1.1", r.RequestLine.HttpVersion)
    
    // Test: Good GET Request line with path
    r, err = RequestFromReader(strings.NewReader("GET /coffee HTTP/1.1\r\nHost: localhost:42069\r\nUser-Agent: curl/7.81.0\r\nAccept: */*\r\n\r\n"))
    require.NoError(t, err)
    require.NotNil(t, r)
    assert.Equal(t, "GET", r.RequestLine.Method)
    assert.Equal(t, "/coffee", r.RequestLine.RequestTarget)
    assert.Equal(t, "1.1", r.RequestLine.HttpVersion)
    
    // Test: Invalid number of parts in request line
    _, err = RequestFromReader(strings.NewReader("/coffee HTTP/1.1\r\nHost: localhost:42069\r\nUser-Agent: curl/7.81.0\r\nAccept: */*\r\n\r\n"))
    require.Error(t, err)
    

When you're done with each lesson, look at the tests in the solution files to see if I did anything you didn't think of.

Don't worry about connecting the request package to any application (cmd) code yet - let's just get our tests passing first.

Run and submit the CLI tests.