HTTP Is Text on a Socket · practice
Send a Request by Hand
The last lesson split bytes that were already in hand. This one produces them: you write the request in wire.py, send it to practice-api through a connect
This lesson runs on Python Land’s servers. The Project tab holds the chapter’s files: wire.py, where you work, fakes.py, which Lessons 5 and 7 use, tests/test_wire.py, which Lesson 7 uses, and README.md, the routes and rules for this chapter. The Shell tab is a
Open wire.py. Its constants include ADDRESS, the server’s name and HOST_LINE; HOOK_TARGET, the target of the request from Lesson 2, which this chapter calls the hook request; and the limits MAX_HEAD and MAX_BODY. make_reader and read_exact are the receive buffer from Networks with Python, already written. The functions after them are yours to write, a few per lesson.
The request line
A request line has three parts separated by single spaces: the GET asks for a resource); the request target, which names the resource on the server (a path, and possibly a query after ?); and the version, HTTP/1.1. CRLF ends the line. An
A server reads the request line by cutting it at the spaces and at the CRLF, so the target must not hold any of them. /v1/items/a b would give the server four words where it expects three, and a target holding \r\n would end the line early and start a request_line refuses such a target with /. Chapter 2 shows how to put any character safely inside a target.
The Host line
The second line is the Host line, which names the server the request is for. wire.py supplies it as HOST_LINE:
HOST_LINE = "Host: practice-api:8080\r\n"
make_request(method, target) joins the request line, HOST_LINE, and one more "\r\n" for the empty line, and encodes the text with "ascii", because a socket sends bytes. For the hook request:
| Line | What it is |
|---|---|
GET /v1/items?page=1&page_size=2 HTTP/1.1\r\n | the request line |
Host: practice-api:8080\r\n | the Host line |
\r\n | the empty line: the head ends here |
The request ends with \r\n\r\n, which tells the server the head is over; this request has no body. Every line ends in CRLF because HTTP/1.1 requires it. Some servers also accept a bare \n, but a tolerant server is no reason to send less than the protocol asks for.
Reading the head as it arrives
The reply’s head ends at the first \r\n\r\n. One recv returns whatever has arrived so far, maybe the whole reply, maybe part of the first line, so reading the head is a
Look for
\r\n\r\ninreader["buffer"]withfind, which returns its position, or-1when it is not there.While it is not there, call
reader["sock"].recv(4096)and add the piece to the end of the buffer, then look again.When it is there, return the bytes before it, and store everything after the empty line back in
reader["buffer"].
Check the buffer before the first recv: if the bytes are already there, there is nothing to wait for. And keep what follows the head. It is the start of the body, which Lesson 5 reads, and a reply often arrives with head and body in one piece.
The first recv in read_head returned b'HTTP/1.1 200 OK\r\nContent-Ty'. What should read_head do?
There are two ways the loop can end without a head, and each gets its own exception:
recvreturnsb"": the server closed the connection before the head ended. RaiseConnectionError, asread_exactdoes.The buffer grows past
limitbytes without a\r\n\r\n: a head that never ends would otherwise fill the program’s memory. RaiseValueError, as yourread_linedid in Networks with Python for a line that never ended.limitdefaults toMAX_HEAD, 16384 bytes.
Sending through connect
fetch puts everything together. Its first two lines open the connection and send the request:
with connect(ADDRESS, timeout=timeout) as sock:
sock.sendall(make_request(method, target))
connect is a socket.create_connection stays on the socket, so every later recv waits at most timeout seconds, 2 by default, and then raises TimeoutError. For now fetch returns the head as bytes; Lesson 5 turns it into a full response.
Your task
In wire.py, write these four functions, in this order:
request_line(method, target): returnf"{method} {target} HTTP/1.1\r\n"; raiseValueErrorif the target does not start with/, or holds a space,\r, or\n.make_request(method, target): return the request line,HOST_LINE, and"\r\n", encoded with"ascii".read_head(reader, *, limit=MAX_HEAD): the loop above.fetch(method, target, *, connect=socket.create_connection, timeout=TIMEOUT): connect toADDRESSwithtimeout=timeout, send the request, make a reader, and return whatread_headreturns.
Leave the functions marked for Lessons 4 to 6 as they are.
Then send the hook request from the Shell tab. main prints whatever fetch returns, so you see the head as a bytes
python wire.py
python wire.py HEAD
The first sends GET with HOOK_TARGET, the second HEAD with the same target. Each prints the head as bytes: the status line first, every line break shown as \r\n, and no body. Both return at once, because read_head stops at the empty line. Nothing from your terminal is checked.
If you want to see what the Host line is for, send the request without it. This command writes the request line and the empty line, and nothing in between:
python -c "import socket, wire; sock = socket.create_connection(wire.ADDRESS, timeout=2.0); sock.sendall((wire.request_line('GET', wire.HOOK_TARGET) + '\r\n').encode('ascii')); print(wire.read_head(wire.make_reader(sock)))"
Leave out the Host line and you should get 400 Bad Request with a short HTML page instead of
If the loop in read_head puzzles you, ask Monty:
Why should my read_head look for \r\n\r\n in the buffer before calling recv, and not only after?
When python wire.py shows the head, press Check my work. The next lesson turns those bytes into a
Task
Write four wire.py, in this order. Check my work names the first one that still raises NotImplementedError, so you meet one at a time.
request_line(method, target)returnsf"{method} {target} HTTP/1.1\r\n". It raisesif targetdoes not start with/, or if it holds a space,\r, or\n.make_request(method, target)returns bytes: the request line,HOST_LINE, and"\r\n", encoded with"ascii".read_head(reader, *, limit=MAX_HEAD)looks for\r\n\r\ninreader["buffer"], and while it is not there, addsreader["sock"].recv(4096)to the end of the buffer and looks again. It returns the bytes before the empty line and leaves every byte after it inreader["buffer"]. It raisesConnectionErrorifrecvreturnsb""first, andValueErrorif the buffer grows pastlimitbytes with no\r\n\r\n.fetch(method, target, *, connect=socket.create_connection, timeout=TIMEOUT)callsconnect(ADDRESS, timeout=timeout)in awithstatement, sendsmake_request(method, target)withsendall, wraps thewith make_reader, and returns whatread_headreturns.
Leave parse_head, read_body, and json_body as they are.
In the python wire.py and python wire.py HEAD.
Then press Check my work. The check calls request_line and make_request with read_head on stand-in sockets holding replies you have not seen, delivered in one piece, one byte at a time, cut in two at every position (including inside the empty line and inside the body), and in pieces of 7 bytes, each time both with a server that closes afterwards and with one that keeps the connection open and sends nothing more. It also checks a buffer that already holds a whole head, a head that never ends, and a server that closes partway through the head. It calls fetch with a stand-in connect and checks the address, the timeout (the default and one you pass), the bytes sent, the head returned, and that the with block ended. It does not read your terminal or contact practice-api.