Start learning free

LESSON 145 OF 188 · BACKEND: TESTS AND RUNNING IT

When Something Breaks

Practice

Every shop has bugs nobody has found yet. When a route crashes, Flask answers 500 Internal Server Error with an HTML page, and the shop's page, which expects JSON, can't read it. In this shop the Gift card (product 8) has no category, so .title() crashes on None.

You replace Flask's page with an error handler, as you did for 404. Flask calls it for every answer with that status, in every route. Here is one for another status, 405, which Flask answers when a route gets a method it doesn't take:

@app.errorhandler(405)
def wrong_method(error):
    return jsonify({"error": "Method not allowed"}), 405

A 500 answer should never carry the error's own text: it names your variables, tables and files, gives an attacker a map of your code, and means nothing to a customer. Send a short message instead, and write the details to the log with logging.error, where only you read them. In the handler, error.original_exception is the error that crashed the route.

The check sends three requests, closes the database for a fourth, and runs your test on the shop and on two broken copies. It must print:

The shop gets three requests:
GET /products/1 -> 200 {"category": "Kitchen", "name": "Mug"}
GET /products/99 -> 404 {"error": "Product not found"}
GET /products/8 -> 500 {"error": "Something went wrong on our side"}
  log from Flask: ERROR Exception on /products/8 [GET], then the traceback
  log: ERROR GET /products/8 failed: 'NoneType' object has no attribute 'title'
The database closes:
GET /products/1 -> 500 {"error": "Something went wrong on our side"}
  log from Flask: ERROR Exception on /products/1 [GET], then the traceback
  log: ERROR GET /products/1 failed: Cannot operate on a closed database.
Your tests on the shop:
  test_crash_answers_json ... ok
  1 test: OK
Your tests on a copy with Flask's HTML error page:
  test_crash_answers_json ... FAIL
    AssertionError: None != {'error': 'Something went wrong on our side'}
  1 test: 1 failed, so they caught this bug
Your tests on a copy that sends the error to the visitor:
  test_crash_answers_json ... FAIL
    AssertionError: {'error': "'NoneType' object has no attribute 'title'"} != {'error': 'Something went wrong on our side'}
  1 test: 1 failed, so they caught this bug

Your task: below get_product, write an error handler for 500, server_error(error). It logs one line with logging.error and the text "%s %s failed: %s", filled with the method, the path and the error that crashed the route, and answers {"error": "Something went wrong on our side"} with the status 500. Then, in ErrorTests, write test_crash_answers_json: get /products/8 and check both the status code and the JSON body.


Assignment
Follow the Coding Tutorial to add the 500 handler and its log line, then a test that checks both the status and the JSON body of a crash.


Hint
The handler works for every route, so it sits on its own below get_product, not inside it. A test that checks only the status code lets Flask's HTML page through, because that is a 500 too: compare response.get_json() with the dictionary as well.


STEP 1 OF 8
    Ctrl + Enter to check