Start learning free

LESSON 145 OF 173 · BACKEND: TESTS AND RUNNING IT

When Something Breaks in C#

Practice

Every shop has bugs nobody has found yet. When a route throws an exception, ASP.NET Core answers 500 Internal Server Error with an empty body. The shop's page expects JSON, so it has nothing to read. In this shop the Gift card (product 8) has no category, so reader.GetString(1) throws on its NULL.

You replace that empty answer with an exception handler: app.UseExceptionHandler runs one function whenever any route throws. Outside a route, nobody returns the result for you, so the function writes it with .ExecuteAsync(context). ASP.NET Core has a similar hook for answers that come out without a body, such as a 404 for an address no route takes:

app.UseStatusCodePages(async statusContext => {
    var context = statusContext.HttpContext;
    if (context.Response.StatusCode == 404) {
        await Results.Json(new { error = "Page not found" }, statusCode: 404).ExecuteAsync(context);
    }
});

A 500 answer should never carry the exception's own message: it names your columns, tables and methods, 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 app.Logger.LogError, where only you read them. In the handler, context.Features.Get<IExceptionHandlerFeature>().Error is the exception 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 ASP.NET Core: ERROR An unhandled exception has occurred while executing the request. (then the stack trace)
  log: ERROR GET /products/8 failed: The data is NULL at ordinal 1. This method can't be called on NULL values. Check using IsDBNull before calling.
The database closes:
GET /products/1 -> 500 {"error": "Something went wrong on our side"}
  log from ASP.NET Core: ERROR An unhandled exception has occurred while executing the request. (then the stack trace)
  log: ERROR GET /products/1 failed: ExecuteReader can only be called when the connection is open.
Your tests on the shop:
  crash answers json ... ok
  1 test: OK
Your tests on a copy without the error handler (ASP.NET Core's empty 500 answer):
  crash answers json ... ERROR
    JsonException: The input does not contain any JSON tokens. Expected the input to start with a valid JSON token, when isFinalBlock is true. Path: $ | LineNumber: 0 | BytePositionInLine: 0.
  1 test: 1 failed, so they caught this bug
Your tests on a copy that sends the error to the visitor:
  crash answers json ... FAIL
    Assert.Equal() Failure: Expected "Something went wrong on our side", Actual "The data is NULL at ordinal 1. This method can't be called on NULL values. Check using IsDBNull before calling."
  1 test: 1 failed, so they caught this bug

Your task: below the route, call app.UseExceptionHandler with a function that keeps the exception in error, logs one line with app.Logger.LogError and the text "{Method} {Path} failed: {Message}", filled with the method, the path and error.Message, and answers {"error": "Something went wrong on our side"} with the status 500. Then, below SetUp, write the test "crash answers json": get /products/8 and check both the status code and the "error" in the JSON body.


Assignment
Follow the Coding Tutorial to add the exception 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 the route, not inside it. A test that checks only the status code lets ASP.NET Core's empty answer through, because that is a 500 too: read the body with ReadFromJsonAsync<JsonElement>() and compare its "error" as well.


STEP 1 OF 9
    Ctrl + Enter to check

    Ops 500 Handler in C#

    Keep going with C#1,600+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free