

0 / 2 embers
0 / 3000 xp
click for more info
Complete a lesson to start your streak
click for more info
Still calibrating
click for more info
Not enough gems
Cost: 6 gems
1: Testing With Testify
incomplete
2: Parsing the Request Line
incomplete
3: Parsing a Stream
incomplete
4: Request Line Review
incomplete
5: State Machine
incomplete
6: Connect the Parsing
incomplete
This lesson's interactive features are locked, please to keep using them
Unfortunately parsing code tends to be just one edge case after another. Remember how I said TCP guarantees data to be in order? That's true, but I never said it had to be complete. TCP (and by extension, HTTP) is a streaming protocol, which means we receive data in chunks and should be able to parse it as it comes in.
So, instead of a full HTTP request, we might just get the first few characters, like this:
GE
We need to manage the state of our parser to handle incomplete reads. For example, maybe in the first pass, our parser only gets:
GE
It needs to be smart enough to know that it's not done yet and keep reading until it gets the full request line:
GET /coffee HTTP/1.1
type chunkReader struct {
data string
numBytesPerRead int
pos int
}
// Read reads up to len(p) or numBytesPerRead bytes from the string per call
// its useful for simulating reading a variable number of bytes per chunk from a network connection
func (cr *chunkReader) Read(p []byte) (n int, err error) {
if cr.pos >= len(cr.data) {
return 0, io.EOF
}
endIndex := cr.pos + cr.numBytesPerRead
if endIndex > len(cr.data) {
endIndex = len(cr.data)
}
n = copy(p, cr.data[cr.pos:endIndex])
cr.pos += n
return n, nil
}
// Test: Good GET Request line
reader := &chunkReader{
data: "GET / HTTP/1.1\r\nHost: localhost:42069\r\nUser-Agent: curl/7.81.0\r\nAccept: */*\r\n\r\n",
numBytesPerRead: 3,
}
r, err := RequestFromReader(reader)
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
reader = &chunkReader{
data: "GET /coffee HTTP/1.1\r\nHost: localhost:42069\r\nUser-Agent: curl/7.81.0\r\nAccept: */*\r\n\r\n",
numBytesPerRead: 1,
}
r, err = RequestFromReader(reader)
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)
Be sure to test values as low as 1 and as high as the length of the request string. Our code should work under all conditions.
If you want additional help, see the Tips section below.
Run and submit the CLI tests.
Implementation help for func (r *Request) parse(data []byte) (int, error):
parseRequestLine.
0 and nil: it needs more data..RequestLine field and change the state to "done".Implementation help for RequestFromReader:
io.ReadAll anymore. Instead, it should create a new buffer: buf := make([]byte, bufferSize, bufferSize). Set bufferSize as a constant at the top of the file, and for now, just a size of 8. We want to test with small buffers to make sure our parser can handle it.readToIndex variable and set it to 0. This will keep track of how much data we've read from the io.Reader into the buffer.Request struct and set the state to "initialized".copy the old data into the new slice.io.Reader into the buffer starting at readToIndex.
io.EOF) set the state to "done" and break out of the loop.readToIndex with the number of bytes you actually readr.parse passing the slice of the buffer that has data that you've actually read so farcopy function and a new slice to do this.readToIndex by the number of bytes that were parsed so that it matches the new length of the buffer.