Consistent error handling with Nancy
In moving the Octopus portal to Nancy, I wanted to have some consistency in the way errors are handled. In ASP.NET MVC/WebAPI I had a number of different filters that would handle different error conditions and return them to the user. I also then had a catch-all in Global.asax that tried to render a friendly error page if something really bad happened.
There are two dimensions to the way errors are handled. First, there are different kinds of errors. Some are the user's fault (not authenticated, no permissions to perform an action, 404, validation failure). Others are our fault (internal server error). I've documented the main error status codes Octopus uses in the API documentation.
Secondly, there are different user experiences that will depend on the error type and request.
- If the client prefers a JSON response, we'll send the status code and a JSON result containing the error details. For 500 exceptions we'll include the exception details; for other errors we'll include a description of what caused the problem and potential solutions.
- If the client prefers a HTML response, we might redirect to a log in screen for 401 errors. Other errors will show a friendly error page describing the problem.
For example, if the user is using a web browser (Accept: text/html
) and they navigate to a page that doesn't exist, they'll get:
While a user of the JSON API (Accept: application/json
) will receive:
And the same will happen if the server encounters an exception while processing the request:
While API clients see:
400: Bad request and 403: Forbidden errors will be handled in a similar way: JSON responses for API clients, and HTML responses for real users.
401 errors will be handled slightly differently. From the API, we'll return a JSON error indicating that the API key is probably invalid, while HTML clients will receive a 302 redirect to the log in page.
Implementation
I've only been playing with Nancy for a couple of days, so the implementation took some experimentation and could probably be much simpler. First, Nancy has very good documentation (I would say more usable than ASP.NET WebAPI), so this page in the Nancy docs was good reading on the subject:
My strategy starts with a custom JsonResponse
called an ErrorResponse
(view the full gist). This can be created from an exception or a single message. For example:
Get["/bad"] = c => { return ErrorResponse.FromMessage("Something bad happened"); };
Get["/buggy"] = c => { return ErrorResponse.FromException(new DivideByZeroException()); };
I then customized my Nancy bootstrapper to log and translate any unhandled exceptions into an ErrorResponse
:
protected override void RequestStartup(ILifetimeScope requestContainer, IPipelines pipelines, NancyContext context)
{
pipelines.OnError.AddItemToEndOfPipeline((z, a) =>
{
log.Error("Unhandled error on request: " + context.Request.Url + " : " + a.Message, a);
return ErrorResponse.FromException(a);
});
base.RequestStartup(requestContainer, pipelines, context);
}
Now that I am capturing error details and turning them into meaningful responses, I need a solution to render the responses in an appropriate way. For this, I implemented a custom IStatusCodeHandler
. My status code handler (full gist) decides whether a HTML response would be more appropriate based on the accept headers, and if so, turns the ErrorResponse
into a new response type to render a HTML error page:
The final piece is my custom Response
that renders the error HTML page from an embedded resource. I put this together based on some existing code in Octopus, so I'm not currently using a real Nancy view engine to do it. I'll be experimenting with turning this view into a Razor view soon.
The final piece of the puzzle is to ensure IIS doesn't attempt to render its own custom error pages instead of mine. To do this, it's as easy as a web.config
entry:
<system.webServer>
<httpErrors errorMode="Custom" existingResponse="PassThrough" />
</.../>
To recap, my error handling strategy includes:
- A custom response type that indicates a meaningful error with details
- A status code handler that translates any other responses and determines whether to render them in HTML
- Another custom response type that renders the friendly error page (may not be required)
Can it be improved? I'm sure it can, since as I said, I've only been playing with Nancy for a few days. Leave a comment in the box below!