0%

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 . This project finishes the client: 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 the rules under “The client in wire.py”. The two new ones:

  • fetch reads a body by content-length, by chunks, or to the close, and reads none after HEAD or with a 1xx, 204, or 304 status. It refuses transfer-encoding together with content-length, and any transfer coding other than chunked.

  • fetch_many sends one request at a time on one connection, asks to close on the last, and raises ConnectionError if 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:

  1. No body for HEAD, for a status below 200, or for 204 or 304: return b"" without reading.

  2. With transfer-encoding: if content-length is there too, raise ValueError. The header must hold exactly one , chunked in any case and with spaces around it allowed; anything else, such as gzip, chunked, raises ValueError, because this client decodes no other coding. Then return what read_chunked returns.

  3. With no content-length: return what read_until_close returns.

  4. Otherwise, read by content-length as 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 and of 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 same recv as 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 header Connection: close, so the server closes once every reply has arrived; the others need no Connection header.

  • 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: close before the last target, the server will read nothing more on this connection. Raise ConnectionError at once, with a message that holds both numbers, how many targets were answered and how many there were, such as the 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 . The connect it hands fetch_many returns a stand-in socket: what your client sends goes through your own /1.1 handler class in this process, and what your server writes comes back from 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.

  1. read_until_close(reader, *, limit=MAX_BODY): return the bytes in reader["buffer"] followed by every byte until recv returns b"", and leave the buffer empty. Raise once the bytes pass limit, before asking for more.

  2. read_body(reader, method, headers, *, status=200), by the rule of Lesson 2: b"" without reading for HEAD and for a status below 200, 204, or 304; with transfer-encoding, ValueError if content-length is also there or if the header is anything but one chunked (any case, spaces allowed), otherwise read_chunked; with no content-length, read_until_close; otherwise content-length as before.

  3. fetch passes status=response["status"] to read_body.

  4. fetch_many(base_url, targets, *, connect=socket.create_connection, timeout=TIMEOUT): one connect call with the host and of base_url (80 when it has none) and timeout, one reader, and for each target in order a GET built with build_request("GET", base_url + target, headers), read in full before the next is sent. Only the last request carries Connection: close. Return the records, one per target, each the parse_head with "body" added, as fetch returns. If a reply says connection: close before the last target, raise ConnectionError with a message that holds both numbers, how many targets were answered and how many there were, such as the server closed the connection after 1 of 3 requests.

Your server must still speak /1.1 and close after every body 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 and bodies you have not seen: replies to 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.