Request Bodies and a JSON API · practice
Say What the Body Is
read_body now hands handle the exact bytes the client sent. Bytes alone do not say what they are: {"title": "Dune"} could be Content-Type header, the same header your server already puts on every reply it sends, content-type: application/json. Before your server treats a body as JSON, it checks two things: that the client labeled it JSON, and that the bytes really are JSON.
The media type
A Content-Type type/subtype, and may add
Content-Type: application/json
Content-Type: Application/JSON; charset=utf-8
Both lines label the body application/json. Media type names are case-insensitive, and charset=utf-8 is a parameter, not part of the type. So the media type is the part before the first ;, with spaces stripped, compared in lowercase.
application/json defines no charset parameter at all: JSON exchanged between systems is charset=utf-8 anyway, and it does no harm, so this server ignores every parameter and always decodes JSON as UTF-8.
What does this print?
print("Application/JSON; charset=UTF-8".split(";")[0].strip().lower())
Compare the whole media type, not a piece of it. "json" in value also accepts application/jsonx, and startswith("application/json") accepts application/json-patch+json. That last one is a real media type: +json (RFC 6839) are JSON with extra rules of their own. This server accepts exactly application/json and refuses the rest. That is its choice, written in SERVICE_CONTRACT.md, not a rule of
In the request request["headers"]["content-type"][0]. A request may have no label at all; request["headers"].get("content-type", [""]) gives a list holding an empty
415 or 400
Two different things can be wrong, and each has its own status:
The label is wrong or missing. The server does not accept that media type, whatever the bytes are: 415 Unsupported Media Type. It never looks at the bytes.
The label is right, the bytes are not. The body is not UTF-8, or it is not valid JSON: 400 Bad Request, a malformed request.
The two statuses tell the client different things to fix. After a 415 it sends a format this server accepts, labeled as such; after a 400 it fixes the bytes it sends. A server that answers both with 400 leaves the client guessing which one went wrong.
json_body(request) is already in reading_server.py: it returns json.loads(request["body"].decode("utf-8")). Two steps can fail inside it. decode raises UnicodeDecodeError on bytes that are not UTF-8, and json.loads raises json.JSONDecodeError on text that is not JSON. Both are subclasses of except ValueError: catches both:
That prints {'title': 'Dune'}, then JSONDecodeError, then UnicodeDecodeError. Keep the except that narrow. except Exception: would also turn a mistake in your own code, such as a misspelled key, into a 400 that blames the client.
Do not decode as Latin-1 to make the error go away. Every byte is a Latin-1 character, so that decode never fails, and bytes that are not UTF-8 would be read as the wrong text instead of being refused.
A body is valid JSON but labeled text/plain. Which status?
Your task
Write body_error(request) in reading_server.py:
Take the media type from the request’s
content-type, as above. If it is notapplication/json, return a 415 response built withjson_error.Call
json_body(request)insidetry. If it raisesValueError, return a 400 response.Otherwise return
None.
Like read_body, it returns None or the response to send. Every error reply comes from json_error, so it carries its own content-type: application/json, even when it refuses the client’s label.
No route calls body_error until Lesson 4, so try it from the Shell tab. make_request in tests/test_server.py builds a request dict with a JSON body, a content-type, and a length; the script changes the label of one request and the bytes of another:
python - <<'EOF'
import reading_server
from tests.test_server import make_request
book = {"title": "Dune", "author": "Frank Herbert"}
print(reading_server.body_error(make_request("POST", "/books", book)))
labeled = make_request("POST", "/books", book)
labeled["headers"]["content-type"] = ["text/plain"]
print(reading_server.body_error(labeled)["status"])
broken = make_request("POST", "/books", book)
broken["body"] = b'{"title": '
print(reading_server.body_error(broken)["status"])
EOF
It prints None, 415, and 400.
Then press Check my work. The next lesson steps back from code to decide where a new book goes and what each write answers.
Task
Check what a request body claims to be before treating it as
In reading_server.py, write body_error(request):
Take the media type of the request’s
content-type: the part of its firstbefore any ;, stripped of spaces and lowercased. A request with nocontent-typehas none. If the media type is not exactlyapplication/json, return a 415 response.Call the supplied
json_body(request)insidetry. If it raises, return a 400 response. Otherwise return
None.
Build both error replies with json_error and a sentence of your own. Catch ValueError only, not Exception.
In the body_error returns for three requests built with make_request from tests/test_server.py.
Then press Check my work. It calls body_error with labels and bodies you have not seen: application/json in other casings and with json, no label at all, bytes that are not