One Connection, Many Requests · capstone
Capstone project: Project: Read Any Body
Your server keeps connections open, and your read_chunked decodes chunks, but your fetch still reads only two kinds of body: none after HEAD, and exactly content-length bytes. Anything else makes it raise fetch reads any body by the rule of Lesson 2, and a new fetch_many, sends several requests on one connection, the way your server now expects them.
What the client must do
SERVICE_CONTRACT.md
fetchreads a body bycontent-length, by chunks, or to the close, and reads none afterHEADor with a 1xx, 204, or 304 status. It refusestransfer-encodingtogether withcontent-length, and any transfer coding other thanchunked.fetch_manysends one request at a time on one connection, asks to close on the last, and raisesConnectionErrorif the server ends the connection early.
Milestones
First, read_until_close(reader, *, limit=MAX_BODY). It returns the bytes already in the buffer and every byte after them, until recv returns b"", and leaves the buffer empty. A reply that ends at the close still gets a limit: raise ValueError once the bytes pass limit, before asking for more. About six lines.
Then read_body(reader, method, headers, *, status=200). Rewrite it by the rule of Lesson 2, in its order:
No body for
HEAD, for a status below 200, or for 204 or 304: returnb""without reading.With
transfer-encoding: ifcontent-lengthis there too, raiseValueError. The header must hold exactly one, chunkedin any case and with spaces around it allowed; anything else, such asgzip, chunked, raisesValueError, because this client decodes no other coding. Then return whatread_chunkedreturns.With no
content-length: return whatread_until_closereturns.Otherwise, read by
content-lengthas before.
The rule from Chapter 1 that refused a body without a length is gone. The tests that came with this chapter in tests/test_wire.py no longer expect it, and the docstring at the top of that file says so.
Then fetch. It passes the reply’s status: read_body(reader, method, response["headers"], status=response["status"]). Without it, a 304 that carries a content-length would leave fetch waiting for bytes that never come.
Run the helper:
python pl_http.py bodies
Your fetch asks the small server from Lesson 3 for /length, /chunked, and /close, one connection each. Before your changes, the chunked reply ends in raised ValueError: this client cannot measure a body sent with Transfer-Encoding yet. After them, each of the three reports ends in a body:
body: b'{}' (2 bytes)
body: b'{"books": ["Kindred", "Caf\xc3\xa9"]}' (31 bytes)
body: b'{"ends": "at the close"}' (24 bytes)
Last, fetch_many(base_url, targets, *, connect=socket.create_connection, timeout=TIMEOUT). It sends GET base_url + target for each target, in order, on one connection, and returns a list of records like the one fetch returns:
Open the connection once,
with connect((host, port), timeout=timeout) as sock:, with the host andof base_url(port 80 when it has none), and make one reader for the whole connection. Bytes of the next reply can arrive in the samerecvas the end of this one, and they wait in that reader’s buffer.Build each request with
build_request("GET", base_url + target, headers). The last one carries the headerConnection: close, so the server closes once every reply has arrived; the others need noConnectionheader.Send a request, read its head and its body, and only then send the next. Sending them all before reading, called pipelining, is something this course leaves alone.
If a reply says
connection: closebefore the last target, the server will read nothing more on this connection. RaiseConnectionErrorat once, with a message that holds both numbers, how many targets were answered and how many there were, such asthe server closed the connection after 1 of 3 requests.
About sixteen lines. Then run it against your own server:
python pl_http.py many
statuses: [200, 200, 200]
/me: b'{"user": null}' (14 bytes)
serve() returned after 1 connection; the server is closed
pl_http prints the three requests as one client -> server line, because it shows everything a connection carried together. Your server’s log has three requests, and serve counted one connection.
Your tests
You can add tests to tests/test_wire.py, as in Chapter 6. fakes.scripted_socket stands for a silent server that keeps the connection open when its script contains None, and for a closed one when the script simply ends. A reply with no length is read until the close, so a test of such a reply ends its script without None:
def test_a_body_that_ends_at_the_close():
sock = scripted_socket([b"HTTP/1.1 200 OK\r\n\r\nab", b"c"])
response = wire.fetch("GET", ITEM_URL, connect=fake_connect(sock, []))
assert response["body"] == b"abc"
This time Check my work runs your tests once, as python -m pytest -x would. None may fail, and none may open a real connection. Run them yourself first:
python -m pytest -q
What is proven, and how
Check my work joins your client to your server without a connect it hands fetch_many returns a stand-in socket: what your client sends goes through your own recv. Once every reply has been read and the connection is still open, recv raises TimeoutError, which catches a client that reads to the close and a server reply with no length. The bodies and many runs are there to watch, not graded.
Then press Check my work. The framing, keep-alive, and close handling that requests does for you on a connection, you have now written by hand. Lesson 5 puts these same bytes inside
Task
Teach wire.py to read any body, and to send several requests on one connection.
read_until_close(reader, *, limit=MAX_BODY): return the bytes inreader["buffer"]followed by every byte untilrecvreturnsb"", and leave the buffer empty. Raiseonce the bytes pass limit, before asking for more.read_body(reader, method, headers, *, status=200), by the rule of Lesson 2:b""without reading forHEADand for a status below 200, 204, or 304; withtransfer-encoding,ValueErrorifcontent-lengthis also there or if the header is anything but onechunked(any case, spaces allowed), otherwiseread_chunked; with nocontent-length,read_until_close; otherwisecontent-lengthas before.fetchpassesstatus=response["status"]toread_body.fetch_many(base_url, targets, *, connect=socket.create_connection, timeout=TIMEOUT): oneconnectcall with the host andof base_url(80 when it has none) andtimeout, one reader, and for each target in order aGETbuilt withbuild_request("GET", base_url + target, headers), read in full before the next is sent. Only the last request carriesConnection: close. Return the records, one per target, each theparse_headwith "body"added, asfetchreturns. If a reply saysconnection: closebefore the last target, raiseConnectionErrorwith a message that holds both numbers, how many targets were answered and how many there were, such asthe server closed the connection after 1 of 3 requests.
Your server must still speak read_body refuses (Lesson 1). Your tests in tests/ must pass: run python -m pytest -q. In the python pl_http.py bodies and python pl_http.py many show your client on a real connection.
Then press Check my work. It calls read_body and read_until_close with readers around a stand-in HEAD, 204, 304, and 101 with nothing to read; chunked bodies with chunked in several cases; lengths; bodies that end at the close, already partly in the buffer; a body over limit; and transfer-encoding with content-length, gzip, chunked, two values, and identity. Where a connection stays open, the stand-in socket raises TimeoutError when more is asked of it than the server sent. It runs fetch on a 304, a 204, a chunked reply, and one that ends at the close. It runs fetch_many against your own server’s HTTP/1.1 handler class, in this process, with books you have not seen, and checks one connection, the request lines, Connection: close on the last request only, the timeout, and every body; then against replies that say connection: close early. It drives your server with three requests on one connection and with a refused chunked body. Last, it runs your tests once, stopping at the first failure, with no network and a limit of 3 seconds. Nothing from your terminal is graded.