The Standard Client and Status Classes · practice
A Timeout Is Not a 504
Every response that arrives now has a class and a body. This lesson is about the request that gets no response. In Python in Practice, a timeout and a 504 Gateway Timeout may have looked like the same failure: something was slow, and the call did not succeed. They are different events, decided by different programs, and a client that reports one as the other sends whoever reads its output looking in the wrong place.
Two scenarios that take their time
Two practice-api routes wait before they answer. /v1/scenarios/slow?delay_ms=250 waits 250 milliseconds and then sends a 200. /v1/scenarios/timeout?delay_ms=750 waits 750 milliseconds and then sends a 504 Gateway Timeout, the status a server sends when something it was waiting for did not answer in time. Here the service plays that part on purpose.
Try the slow one first, with a timeout of one second:
python -c "import wire; print(wire.get_json('/v1/scenarios/slow?delay_ms=250', timeout=1.0)['status'])"
It prints 200 after a short pause. A quarter of a second fits inside one second, so the wait was nothing more than a wait.
Now the timeout scenario, twice, with two different timeouts:
python -c "import wire; print(wire.get_json('/v1/scenarios/timeout?delay_ms=750', timeout=2.0)['status'])"
python -c "import wire; print(wire.get_json('/v1/scenarios/timeout?delay_ms=750', timeout=0.5)['status'])"
With two seconds to wait, your client receives the 504 and prints 504. With half a second, it gives up before the server answers: the second command ends with a TimeoutError: timed out. Same request, same server; only your timeout changed. If the first command also raises TimeoutError, the reply took longer than two seconds to come back to your sandbox; the point below does not depend on it.
Who decided
The two runs look like this on a timeline. Compare where each line ends, and who ended it:

With timeout=0.5, half a second passes with no head on the connection, and http.client raises TimeoutError from getresponse. No status, no headers and no body exist; your client decided to stop. With timeout=2.0, the server’s reply arrives after three quarters of a second. It is a complete response like any other, with a status line, headers and a get_json returns it with "class": "server error".
So the question “who gave up?” has a different answer in each case. A 504 means a server gave up on something further along and told you so. A TimeoutError means your client gave up on the server, and the server may still be working on the request. The first is a fact about the server; the second is a fact about your timeout.
With timeout=0.5, a GET of /v1/scenarios/timeout?delay_ms=750 raises TimeoutError. Who gave up?
report: say which one happened
report(target, *, make_connection, timeout) returns one line that says what happened to a GET of target:
when
get_jsonreturns a record, the status and its class, as in504 server erroror200 success;when
get_jsonraisesTimeoutError, a line that says the client timed out and how long it waited, and that names no status, because none arrived.
The try goes around the call to get_json, and the except names only TimeoutError. That is the narrow boundary from Python in Practice: catch only the
except OSErroralso catchesConnectionRefusedError, because both are kinds ofOSError. A refused connection would then be reported as a timeout, which sends someone looking for a slow server when no server was listening.except Exceptionalso catches theValueErrorthatget_jsonraises for a body labeled JSON that is not JSON, and would report a broken response as a timeout.
Both of those failures should leave report as exceptions, so whoever called it sees what really happened.
get_json itself needs no change. Since Lesson 1, TimeoutError leaves it through the with contextlib.closing(...) block, which closes the connection on the way out, and report is the only place that turns it into a line. Do not catch it inside get_json: there is no response to put in a record.
Your task
In
wire.py, writereport(target, *, make_connection, timeout)as described above. Passmake_connectionandtimeouton toget_json.Run it on the timeout scenario with both timeouts:
python -c "import wire; print(wire.report('/v1/scenarios/timeout?delay_ms=750', timeout=0.5))"
python -c "import wire; print(wire.report('/v1/scenarios/timeout?delay_ms=750'))"
The first line says your client timed out after 0.5 seconds, in your own words; the second reads 504 server error. If the second also says your client timed out, the 504 took longer than two seconds to reach your sandbox, as in the earlier run; report told the truth both times.
Then press Check my work. Your client now has an answer for every response and for no response. The next lesson proves all of it with tests of your own.
Task
Write report(target, *, make_connection, timeout) in wire.py. It calls get_json(target, make_connection=make_connection, timeout=timeout) and returns one line of text:
when a response arrives, the status and its class, starting with the status, as in
504 server error;when
get_jsonraisesTimeoutError, a line containing the wordstimed outand the timeout it waited, such as0.5, and no.
Catch only TimeoutError. A ConnectionRefusedError, or the report as an exception. get_json keeps letting TimeoutError through, with the connection closed.
Run report on /v1/scenarios/timeout?delay_ms=750 with timeout=0.5 and with the default timeout, as the lesson shows.
Then press Check my work. It calls report and get_json with a stand-in for make_connection that hands back a scripted 504 or 200, raises TimeoutError from getresponse() for a silent server, raises ConnectionRefusedError from request(), or hands back a body labeled JSON that is not JSON. It checks each line, that the timeout reached the connection, that the connection was closed after the timeout, and that the other two failures were not caught. Your wording around timed out is yours. Nothing from your