0%

The Standard Client and Status Classes · practice

Classify Every Scenario

get_json reads the status and the headers a decision needs. Now it can apply the class rule from Lesson 2 to every response, the failures included. practice-api has routes that fail on purpose, so you can watch a client meet them without breaking anything. This lesson writes classify, adds the class to the record, and decodes the body of every response, error bodies included.

Four ways to fail

Each of these targets fails in a different way:

  • /v1/items/99: an item that does not exist;

  • /v1/scenarios/not-found: a 404 sent on purpose;

  • /v1/scenarios/server-error: a 500 sent on purpose;

  • /v1/items?page=11&page_size=3: a page number out of .

Every one of them comes with a JSON body, and the bodies have one shape: an "error" holding a "code" a program can test, a "message" a person can read, and sometimes more. These are the four bodies practice-api sends:

{"error":{"code":"not_found","message":"item not found","scenario":"resource"}}
{"error":{"code":"scenario_not_found","message":"intentional not found scenario","scenario":"not_found"}}
{"error":{"code":"scenario_server_error","message":"intentional server error scenario","scenario":"server_error"}}
{"error":{"code":"invalid_parameter","message":"page must be between 1 and 10"}}

Look at the last one. The status, 400, says only that the request was wrong. The body says which part, and what would fix it: pages run from 1 to 10. A client that raises on every 4xx before reading the body, as raise_for_status did, loses exactly that sentence. Two of the bodies are 404s, and only their "code" tells you that one item is missing while the other route failed on purpose.

Why should a client keep the body of a 404?

The classify function

classify(status) returns the class of any code as a word a person can read:

status // 100classify returns
1"informational"
2"success"
3"redirection"
4"client error"
5"server error"

Anything below 100 or above 599 is not a , and classify raises for it. The rule is the first digit for every code in the range, including codes no of yours names: 299 is a "success" and 418 a "client error". One way to write it is a list of the five names, indexed with status // 100 - 1; an and elif chain over status // 100 works as well.

A data key for every JSON body

get_json adds two keys to its record:

  • "class": classify(status), for every response.

  • "data": the body decoded with json.loads(body.decode("utf-8")) when the response is labeled JSON and the body is not empty; otherwise None.

“Labeled JSON” means the media type is application/json. The content_type from Lesson 3 may carry a , so take the part before any ; with .split(";")[0], .strip() the spaces, and compare in lowercase, since media ignore case. A reply with no Content-Type, a text/plain reply, and an empty body all leave "data" as None, and "body" still holds the bytes.

The status plays no part in that decision. A 404 labeled JSON is decoded exactly like a 200, which is how the error body reaches your caller. And get_json raises nothing because of a status: a 500 is a response like any other, with a class of "server error".

One case does raise. A reply labeled JSON whose body is not JSON makes json.loads raise json.JSONDecodeError, a kind of ValueError. Let it through: the label and the body disagree, and the caller should hear about it. Decode after the with block or inside it; either way the connection is closed when the exception leaves get_json.

Your task

  1. In wire.py, write classify(status).

  2. Add "class" and "data" to the record get_json returns, as described above. Keep everything Lessons 1 and 3 wrote.

  3. Run all four targets:

python -c "
import wire
for target in ['/v1/items/99', '/v1/scenarios/not-found', '/v1/scenarios/server-error', '/v1/items?page=11&page_size=3']:
    record = wire.get_json(target)
    print(record['status'], record['class'], record['data'])
"

With your code written, it prints:

404 client error {'error': {'code': 'not_found', 'message': 'item not found', 'scenario': 'resource'}}
404 client error {'error': {'code': 'scenario_not_found', 'message': 'intentional not found scenario', 'scenario': 'not_found'}}
500 server error {'error': {'code': 'scenario_server_error', 'message': 'intentional server error scenario', 'scenario': 'server_error'}}
400 client error {'error': {'code': 'invalid_parameter', 'message': 'page must be between 1 and 10'}}

Then press Check my work. Every response that arrives now has a class and keeps its body. The next lesson meets the case where no response arrives at all.

Task

Sort every response by class and keep every body.

Write classify(status) in wire.py. It returns "informational", "success", "redirection", "client error", or "server error" from the first digit of the code, status // 100, for any code from 100 to 599, and raises for anything outside that .

Add two keys to the record get_json returns:

  • "class": classify(status), for every response.

  • "data": the body decoded with json.loads(body.decode("utf-8")) when the media type of content_type (the part before any ;, compared in lowercase) is application/json and the body is not empty; otherwise None.

get_json raises nothing because of a status: a 404 or a 500 returns its record, with its body decoded like any other. A body labeled JSON that is not JSON lets the ValueError from json.loads through, with the connection closed. Keep everything Lessons 1 and 3 wrote.

Run the four failing targets in the , as the lesson shows.

Then press Check my work. It calls classify with codes from every class, including codes such as 199 and 299 that no of names covers, and with 99, 600, and 1000. It calls get_json with a stand-in for make_connection whose scripted responses you have not seen: 400, 404, 429, 500, and 503 with JSON error bodies, a 200 labeled application/json without , a text/plain 418, an empty JSON body, a reply with no Content-Type, and a reply labeled JSON whose body is HTML. Nothing from your terminal is graded.