0%

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 whose last line is 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 the client gives up at 0.5 seconds and no response exists; with timeout=2.0 a complete 504 arrives at 0.75 seconds.

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 body, and the server chose to send it. Your 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_json returns a record, the status and its class, as in 504 server error or 200 success;

  • when get_json raises TimeoutError, 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 whose meaning you know and have an answer for. Two wider choices look tempting and are wrong here:

  • except OSError also catches ConnectionRefusedError, because both are kinds of OSError. A refused connection would then be reported as a timeout, which sends someone looking for a slow server when no server was listening.

  • except Exception also catches the ValueError that get_json raises 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

  1. In wire.py, write report(target, *, make_connection, timeout) as described above. Pass make_connection and timeout on to get_json.

  2. 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_json raises TimeoutError, a line containing the words timed out and the timeout it waited, such as 0.5, and no .

Catch only TimeoutError. A ConnectionRefusedError, or the from a body labeled that is not JSON, must leave 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 is graded, and no check measures time.