0%

requests and httpx, Now That You Know · practice

What Each Client Sends Unasked

In Chapter 2 you wrote every line of a request yourself, and wire.py still sends exactly what you tell it: the request line, a Host line from host_header, the headers the caller passes, and Content-Length when there is a body. Nothing else. The libraries are more generous. Each one adds lines you never asked for, and now that you know what every header does, you can read them.

Four clients, one echo

To see those lines you need a server that reports what it received. practice-api’s /v1/headers reports only X-Practice-Name, as you saw in Lesson 2.4, so pl_http brings its own: a small echo on 127.0.0.1 that answers GET /echo with every of the request, as , in the order they arrived. It runs only while pl_http runs.

Open side_by_side.py and read its last three . fetch_echoes(url) sends one GET to the echo with each of four clients, wire.fetch, a plain http.client.HTTPConnection, a requests session, and an httpx client, and each asks for exactly one header, X-Asked: yes. echo_headers turns each reply into a of name and . main(argv) takes the echo’s as its one , calls fetch_echoes, and prints, for each client, the names your unasked finds. pl_http passes the URL, because the echo exists only inside its run:

python pl_http.py echo

Before you write unasked, the run still shows the four requests as the echo received them, then stops where main calls your function: raised NotImplementedError: not written yet: Lesson 5. pl_http writes PORT where the the system chose stood:

request 1  GET /echo  (connection 1)
  Host: 127.0.0.1:PORT
  X-Asked: yes
request 2  GET /echo  (connection 2)
  Host: 127.0.0.1:PORT
  Accept-Encoding: identity
  X-Asked: yes
request 3  GET /echo  (connection 3)
  Host: 127.0.0.1:PORT
  User-Agent: python-requests/VERSION
  Accept-Encoding: gzip, deflate, zstd
  Accept: */*
  Connection: keep-alive
  X-Asked: yes
request 4  GET /echo  (connection 4)
  Host: 127.0.0.1:PORT
  Accept: */*
  Accept-Encoding: gzip, deflate
  Connection: keep-alive
  User-Agent: python-httpx/VERSION
  X-Asked: yes

VERSION stands for the version of the library installed here. Request 1 is wire.py, request 2 is http.client, request 3 is requests, and request 4 is httpx. wire.py added one line, http.client two, and each library five, in an order of its own.

What each line is for

Host. Every client sends it, because /1.1 requires it, as Lesson 2.4 showed. wire.py writes it with host_header; http.client writes it from the host and port it was given, and requests leaves it to http.client underneath; httpx writes it itself.

User-Agent. The client’s name and version. A server can log it, count which clients call it, or work around a known bug in one version. Your server ignores it. Only the two libraries send one; http.client and wire.py leave it out.

Why does User-Agent carry a version number?

Accept. Which media the client will take. */* means any. Your make_session from Lesson 1 replaced it with application/json for the reading client.

Accept-Encoding. An offer: “you may compress the body with one of these, and I will undo it”. gzip and deflate are common compression formats; zstd is a newer one, which requests offers on Python 3.14 because that Python can decode it (on an older Python the list ends at deflate). A server that takes the offer sends a compressed body with a content-encoding header naming the format, and requests or httpx decompress it before .content or .json() gives it to you. http.client offers identity, which means “no compression, please”, because it does not decompress. wire.py sends no Accept-Encoding line at all. HTTP reads that as “any coding is fine”, so a server could still compress, and wire.py would hand you the compressed bytes; servers usually compress only for a client that offered. Your server never compresses, so none of this changes its replies.

requests sends accept-encoding: gzip, deflate, zstd. What does that allow the server to do?

Connection. Whether the client wants the connection kept for another request, Chapter 9’s question. A requests session and an httpx client keep a pool, so they say keep-alive out loud. http.client and wire.py say nothing, and under HTTP/1.1 saying nothing means the same thing: the connection may stay open. In this run each client opens a connection of its own, and the echo closes each one after its reply, so every request shows a new connection number.

http.client sends no Connection line; requests sends Connection: keep-alive. What may the server assume about the http.client request's connection?

Header names ignore case, so Accept and accept are the same header, and a client may send them in any order. To compare what each client added, then, compare names in one case and ignore their order. That is the function you write.

Your task

In side_by_side.py, write unasked(received, asked). received is something whose gives header names, such as the dict echo_headers returns; asked is a list of the names you sent yourself. Return a list of the names in received that are not in asked, each in lowercase, in the order received gives them. Compare in one case: lowercase every name in asked and every name in received before comparing. A name in asked that never arrived is not something the client added, so it never appears in the result.

A loop over asked that builds a list of lowercase names, then a loop over received that keeps the ones not in it, is enough. Then run python pl_http.py echo again. After the same four requests, main prints:

standard output:
  wire.py added: host
  http.client added: host, accept-encoding
  requests added: host, user-agent, accept-encoding, accept, connection
  httpx added: host, accept, accept-encoding, connection, user-agent

The result block ends with the echo answered 4 requests, one per connection, and is closed.

Press Check my work. Lesson 6 puts requests to work in a real program: the reading list client.

Task

Find the header names a client added on its own.

In side_by_side.py, write unasked(received, asked):

  • received gives header names when you over it, such as the echo_headers returns.

  • asked is a of the names you sent yourself.

  • Return a list of the names in received that are not in asked, each lowercased, in the order received gives them.

  • Ignore case on both sides: X-Trace in received matches x-TRACE in asked.

  • A name in asked that is not in received never appears in the result.

Return the names, not the values.

In the , python pl_http.py echo serves a small echo on 127.0.0.1 and runs your side_by_side.py main() with its : four clients each send one request, the echo shows the it received, and main prints what your unasked says each client added. The run shows the four requests even before unasked exists.

Then press Check my work. It calls unasked with header dicts you have not seen, with names in mixed case on both sides, an empty asked, and an asked name that never arrived. While it runs, side_by_side’s requests, httpx, http, and wire refuse to send anything, so no request leaves the check. Nothing from your terminal is graded.