JSON is the expected content type of modern APIs, and exceptions should be no different. Learn how to build a middleware component that will serialize unhandled exceptions in a friendly way.
- [Instructor] By default, ASP.NET Core returns 500 internal server error and an empty response body if an unhandled exception occurs in your API. We can improve this by creating an exception filter that will catch all unhandled exceptions and send the error message back to the client as a JSON response. I'm going to stop my API so we can add some files. First, let's create a class that will represent the error message itself. I'm going to create a new folder called Models to hold classes that represent response models, and create a class called APIError.
An error message returned from the API will have a string that's a message and a detail string, as well. And, if the app is running in Development, we can return a StackTrace. I'm going to add some JSON.net attributes to the StackTrace property so that it disappears if it's null. I can add the JSON property element and set NullValueHandling to NullValueHandling.Ignore.
I can also set DefaultValueHandling to DefaultValueHandling.Ignore. This works because ASP.NET Core uses JSON.net under the hood. I'm going to add one more attribute here called DefaultValue, and this is in a different namespace, the Component Model namespace, and just say the DefaultValue is blank so that if, for some reason, the StackTrace is empty, it just won't be added to the response. Next, I need to create an exception filter. A filter is a chunk of code that runs before or after ASP.NET Core processes a request.
Filters that handle errors and exceptions are called Exception Filters, and there's a special interface we can implement. I'm going to create another new folder called Filters, and a class called JsonExceptionFilter. This class can implement IExceptionFilter, which is part of the Mvc.filters namespace. This interface has one method we need to implement: OnException. This is the code we want to run any time there's an unhandled exception in the API.
So what we want to do is create a new instance of the API error class and then just serialize it and send it back to the client. So I can say error equals new ApiError, which is in the models namespace. I need to import that, and I can use the exception context, which contains the actual exception object to add some properties to this. I can say error.Message is the context.Exception.Message, and the Detail is the context.Exception.StackTrace.
Now I want to send this response back to the client, and I can do that by setting the result property on the context. I need to manually create a class called ObjectResult, which is the Mvc namespace. I'm going to serialize this error class that we created and I also want to explicitly say, I want to keep that status code of 500. Returning the StackTrace is nice for development, but we don't want to return that detail if the API is running in production. We can detect the current environment at runtime with the IHostingEnvironment helper service.
I can inject that using constructor injection. I can take a type of IHostingEnvironment in my constructor for the filter, which is in the hosting namespace, and I'll need a local variable to hold that. Of type IHostingEnvironment, and the constructor, I can just set that. Now, in my OnException method, I can check whether we're running in development or staging our production.
So I can check if were currently IsDevelopment, then we want to get that StackTrace information, 'cause that's helpful while we're developing the API. But, if we're running in production, we want to return a more generic error message to the user or the client. So, we'll just say "A server error occurred" and we can also pass, as the detail, the exception message. Now that we've written an exception filter, we need to tell Mvc to use it.
I can do that in the startup class. In the AddMvc method in configure services, I can edit the Filters collection and add our new JSON exception filter. I need to add this by type, so I'll use typeof(JsonExceptionFilter). You may need to import the namespace for this new class. Now we can try it out. Our rooms controller is still throwing a NotImplemented exception, so we can test this behavior in Postman.
If I send a Get Request to slash rooms, my code will still throw a NotImplemented exception. If I let this continue, then we'll get 500 internal server error back, but this time, instead of an empty body, we'll get the message and detail properties that we set before. So now we have a nicely formatted JSON response that includes an error message and some StackTrace information. That's much better than just an empty body. That covers the basics of setting up our project. We'll keep adding to this API throughout the course.
- REST vs. RPC
- Using HTTP methods (aka verbs)
- Returning JSON
- Creating a new API project
- Building a root controller
- Routing to controllers with templates
- Requiring HTTPS for security
- Creating resources and data models
- Returning data and resources from a controller
- Representing links (HREFs)
- Representing collections
- Sorting and searching collections
- Creating forms
- Caching and compression
- Authentication and authorization for RESTful APIs