When working with ASP.NET Web Api from a .NET client, one of the more confounding things can be handling the case where errors are returned from the Api. Specifically, unwrapping the various types of errors which may be returned from a specific API action method, and translating the error content into meaningful information for use be the client.
How we handle the various types of errors that may be returned to our Api client applications can be very dependent upon specific application needs, and indeed, the type of client we are building.
Image by Damien Roué | Some Rights Reserved
In this post we'll look at some general types of issues we might run into when handing error results client-side, and hopefully find some insight we can apply to specific cases as they arise.
Most Web Api Action methods will return one of the following:
- Void: If the action method returns void, the HTTP Response created by ASP.NET Web Api will have a 204 status code, meaning "no content."
- HttpResponseMessage: If the Action method returns an
HttpResponseMessage
, then the value will be converted directly into an HTTP response message. We can use the Request.CreateResponse()
method to create instances of HttpResponseMessage
, and we can optionally pass domain models as a method argument, which will then be serialized as part of the resulting HTTP response message.
- IHttpActionResult: Introduced with ASP.NET Web API 2.0, the
IHttpActionResult
interface provides a handy abstraction over the mechanics of creating an HttpResponseMessage
. Also, there are a host of pre-defined implementations for IHttpActionResult defined in System.Web.Http.Results
, and the ApiController
class provides helper methods which return various forms of IHttpActionResult
, usable directly within the controller.
- Other Type: Any other return type will need to be serialized using an appropriate media formatter.
For more details on the above, see Action Results in Web API 2 by Mike Wasson.
From Web Api 2.0 onward, the recommended return type for most Web Api Action methods is IHttpActionResult unless this type simply doesn't make sense.
To keep things general and basic, let's start by spinning up a standard ASP.NET Web Api project using the default Visual Studio Template. If you are new to Web Api, take a moment to review the basics, and get familiar with the project structure and where things live.
Make sure to update the Nuget packages after you create the project.
Next, let's put together a very rudimentary client application. Open another instance of Visual Studio, and create a new Console application. Then, use the Nuget package manager to install the ASP.NET Web Api Client Libraries into the solution.
We're going to use the simple Register() method as our starting point to see how we might need to unwrap some errors in order to create a more useful error handling model on the client side.
If we return to our Web Api project and examine the Register()
method, we see the following:
The Register() method from AccountController:
[AllowAnonymous]
[Route("Register")]
public async Task<IHttpActionResult> Register(RegisterBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var user = new ApplicationUser()
{
UserName = model.Email,
Email = model.Email
};
IdentityResult result = await UserManager.CreateAsync(user, model.Password);
if (!result.Succeeded)
{
return GetErrorResult(result);
}
return Ok();
}
In the above, we can see that there are a number of options for what might be returned as our IHttpActionResult
.
First, if the Model state is invalid, the BadRequest()
helper method defined as part of the ApiController
class will be called, and will be passed the current ModelStateDictionary
. This represents simple validation, and no additional processes or database requests have been called.
If the Mode State is valid, the CreateAsync()
method of the UserManager
is called, returning an IdentityResult
. If the Succeeded property is not true, then GetErrorResult()
is called, and passed the result of the call to CreateAsync()
.
GetErrorResult()
is a handy helper method which returns the appropriate IHttpActionResult
for a given error condition.
The GetErrorResult Method from AccountController
private IHttpActionResult GetErrorResult(IdentityResult result)
{
if (result == null)
{
return InternalServerError();
}
if (!result.Succeeded)
{
if (result.Errors != null)
{
foreach (string error in result.Errors)
{
ModelState.AddModelError("", error);
}
}
if (ModelState.IsValid)
{
return BadRequest();
}
return BadRequest(ModelState);
}
return null;
}
From the above, we can see we might get back a number of different responses, each with a slightly different content, which should assist the client in determining what went wrong.
So, let's see some of the ways things can go wrong when making a simple POST request to the Register()
method from our Console client application.
Add the following code to the console application. Note that we are intentionally making a flawed request. We will pass a valid password and a matching confirmation password, but we will pass an invalid email address. We know that Web Api will not like this, and should kick back a Model State Error as a result.
Flawed Request Code for the Console Client Application:
static void Main(string[] args)
{
string email = "john";
string password = "Password@123";
string confirmPassword = "Password@123";
HttpResponseMessage result =
Register(email, password, confirmPassword);
if(result.IsSuccessStatusCode)
{
Console.WriteLine(
"The new user {0} has been successfully added.", email);
}
else
{
Console.WriteLine(result.ReasonPhrase);
}
Console.Read();
}
public static HttpResponseMessage Register(
string email, string password, string confirmPassword)
{
using (var client = new HttpClient())
{
var response =
client.PostAsJsonAsync("http://localhost:51137/api/Account/Register",
new
{
Email = email,
Password = password,
ConfirmPassword = confirmPassword
}).Result;
return response;
}
}
If we run our Web Api application, wait for it to spin up, and then run our console app, we see the following output:
Console output from the flawed request:
Bad Request
Well, that's not very helpful.
If we de-serialize the response content to a string, we see there is more information to be had. Update the Main()
method as follows:
De-serialize the Response Content:
static void Main(string[] args)
{
string email = "john";
string password = "Password@123";
string confirmPassword = "Password@123";
HttpResponseMessage result =
Register(email, password, confirmPassword);
if(result.IsSuccessStatusCode)
{
Console.WriteLine(
"The new user {0} has been successfully added.", email);
}
else
{
string content = result.Content.ReadAsStringAsync().Result;
Console.WriteLine(content);
}
Console.Read();
}
Now, if we run the Console application again, we see the following output:
Output from the Console Application with De-Serialized Response Content:
{"Message":"The request is invalid.","ModelState":{"":["Email 'john' is invalid."]}}
Now, what we see above is JSON. Clearly the JSON object contains a Message
property and a ModelState
property. But the ModelState
property, itself another JSON object, contains an unnamed property, an array containing the error which occurred when validating the model.
Since a JSON object is essentially nothing but a set of key/value pairs, we would normally expect to be able to unroll a JSON object into a Dictionary<string, object>
. However, the nameless property(ies) enumerated in the ModelState dictionary on the server side makes this challenging.
Unwrapping such an object using the Newtonsoft.Json library is doable, but slightly painful. Equally important, an error returned from our API may, or may not have a ModelState dictionary associated with it.
Say we figured out that we need to provide a valid email address when we submit our request to the Register()
method. Suppose instead, we are not paying attention and instead enter two slightly different passwords, and also forget that passwords have a minimum length.
Modify the code in the Main()
method again as follows:
Flawed Request with Password Mismatch:
{
"Message":"The request is invalid.",
"ModelState": {
"model.Password": [
"The Password must be at least 6 characters long."],
"model.ConfirmPassword": [
"The password and confirmation password do not match."]
}
}
In this case, it appears the items in the ModelState Dictionary are represented by valid key/value pairs, and the value for each key is an array.
We've seen a few examples of what can happen when the model we are passing with our POST request is invalid. But what happens if our Api is unavailable?
Let's pretend we finally managed to get our email and our passwords correct, but now the server is off-line.
Stop the Web Api application, and then re-run the Console application. Of course, after a reasonable server time-out, our client application throws an AggregateException
.
What's an AggregateException
? Well, it is what we get when an exception occurs during execution of an async method. If we pretend we don't know WHY our request failed, we would need to dig down into the InnerExceptions
property of the AggregateException
to find some useful information.
In the context of our rudimentary Console application, we will implement some top-level exception handling so that our Console can report the results of any exceptions like this to us.
Update the Main()
method once again, as follows:
Add Exception Handling to the Main() Method of the Console Application:
static void Main(string[] args)
{
string email = "john@example.com";
string password = "Password@123";
string confirmPassword = "Password@123";
try
{
HttpResponseMessage result =
Register(email, password, confirmPassword);
if (result.IsSuccessStatusCode)
{
Console.WriteLine(
"The new user {0} has been successfully added.", email);
}
else
{
string content = result.Content.ReadAsStringAsync().Result;
Console.WriteLine(content);
}
}
catch (AggregateException ex)
{
Console.WriteLine("One or more exceptions has occurred:");
foreach (var exception in ex.InnerExceptions)
{
Console.WriteLine(" " + exception.Message);
}
}
Console.Read();
}
If we run our console app now, while our Web Api application is offline, we get the following result:
Console Output with Exception Handling and Server Time-Out:
One or more exceptions has occurred:
An error occurred while sending the request.
Here, we are informed that "An error occurred while sending the request" which at least tells us something, and averts the application crashing due to an unhandled AggregateException
.
We've seen a few different varieties of errors and exceptions which may arise when registering a user from our client application.
While outputting JSON from the response content is somewhat helpful, I doubt it's what we are looking for as Console output. What we need is a way to unwrap the various types of response content, and display useful console messages in a clean, concise format that is useful to the user.
While I was putting together a more in-depth, interactive console project for a future article, I implemented a custom exception, and a special method to handle these cases.
Yeah, yeah, I know. Some of the cases above don't technically represent "Exceptions" by the hallowed definition of the term. In the case of a simple console application, however, a simple, exception-based system makes sense. Further, unwrapping all of our Api errors up behind a single abstraction makes it easy to demonstrate how to unwrap them.
Mileage may vary according to the specific needs of YOUR application. Obviously, GUI-based applications may extend or expand upon this approach, relying less on Try/Catch and throwing exceptions, and more upon the specifics of the GUI elements available.
Add a class named ApiException
to the Console project, and add the following code:
ApiException - a Custom Exception
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
namespace ApiWithErrorsTest
{
public class ApiException : Exception
{
public HttpResponseMessage Response { get; set; }
public ApiException(HttpResponseMessage response)
{
this.Response = response;
}
public HttpStatusCode StatusCode
{
get
{
return this.Response.StatusCode;
}
}
public IEnumerable<string> Errors
{
get
{
return this.Data.Values.Cast<string>().ToList();
}
}
}
}
Next, let's add a method to our Program which accepts an HttpResponseMessage
as a method argument, and returns an instance of ApiException
. Add the following code the the Program class of the Console application:
Add the CreateApiException Method the to Program Class:
public static ApiException CreateApiException(HttpResponseMessage response)
{
var httpErrorObject = response.Content.ReadAsStringAsync().Result;
var anonymousErrorObject =
new { message = "", ModelState = new Dictionary<string, string[]>() };
var deserializedErrorObject =
JsonConvert.DeserializeAnonymousType(httpErrorObject, anonymousErrorObject);
var ex = new ApiException(response);
if (deserializedErrorObject.ModelState != null)
{
var errors =
deserializedErrorObject.ModelState
.Select(kvp => string.Join(". ", kvp.Value));
for (int i = 0; i < errors.Count(); i++)
{
ex.Data.Add(i, errors.ElementAt(i));
}
}
else
{
var error =
JsonConvert.DeserializeObject<Dictionary<string, string>>(httpErrorObject);
foreach (var kvp in error)
{
ex.Data.Add(kvp.Key, kvp.Value);
}
}
return ex;
}
In the above, we get a sense for what goes into unwrapping an HttpResponseMessage
which contains a mode state dictionary.
When the response content includes a property named ModeState
, we unwind the ModelState
dictionary using the magic of LINQ. We knit the string key together with the contents of the value array for each item present, and then add each item to the exception Data dictionary using an integer index for the key.
If no ModelState
property is present in the response content, we simply unwrap the other errors present, and add them to the Data dictionary of the exception.
We've already added some minimal exception handling at the top level of our application. Namely, we have caught and handled AggregateExceptions
which may be thrown by async calls to our api, which are not handled deeper in the call stack.
Now that we have added a custom exception, and a method for unwinding certain types error responses, let's add some additional exception handling, and see if we can do a little better, farther down.
Update the Register()
method as follows:
Add Handle Errors in the Register() Method:
public static HttpResponseMessage Register(
string email, string password, string confirmPassword)
{
using (var client = new HttpClient())
{
var response =
client.PostAsJsonAsync("http://localhost:51137/api/Account/Register",
new
{
Email = email,
Password = password,
ConfirmPassword = confirmPassword
}).Result;
if(!response.IsSuccessStatusCode)
{
var ex = CreateApiException(response);
throw ex;
}
return response;
}
}
You can see here, we are examining the HttpStatusCode
associated with the response, and if it is anything other than successful, we call our CreateApiException()
method, grab the new ApiException, and then throw.
In reality, for this simple console example we likely could have gotten by with creating a plain old System.Exception
instead of a custom Exception implementation. However, for anything other than the simplest of cases, the ApiException
will contain useful additional information.
Also, the fact that it is a custom exception allows us to catch ApiException
and handle it specifically, as we will probably want our application to behave differently in response to an error condition in an Api response than we would other exceptions.
Now, all we need to do (for our super-simple example client, anyway) is handle ApiException
specifically in our Main()
method.
Now we want to be able to catch any flying ApiExceptions in Main()
. Our Console application, shining example of architecture and complex design requirements that it is, pretty much only needs a single point of error handling to properly unwrap exceptions and write them out as console text!
Add the following code to Main()
:
Handle ApiException in the Main() Method:
static void Main(string[] args)
{
string email = "john@example.com";
string password = "Password@123";
string confirmPassword = "Password@123";
try
{
HttpResponseMessage result =
Register(email, password, confirmPassword);
if (result.IsSuccessStatusCode)
{
Console.WriteLine(
"The new user {0} has been successfully added.", email);
}
else
{
string content = result.Content.ReadAsStringAsync().Result;
Console.WriteLine(content);
}
}
catch (AggregateException ex)
{
Console.WriteLine("One or more exceptions has occurred:");
foreach (var exception in ex.InnerExceptions)
{
Console.WriteLine(" " + exception.Message);
}
}
catch(ApiException apiEx)
{
var sb = new StringBuilder();
sb.AppendLine(" An Error Occurred:");
sb.AppendLine(string.Format(" Status Code: {0}", apiEx.StatusCode.ToString()));
sb.AppendLine(" Errors:");
foreach (var error in apiEx.Errors)
{
sb.AppendLine(" " + error);
}
Console.WriteLine(sb.ToString());
}
Console.Read();
}
All we are doing in the above is unwinding the ApiException
and transforming the contents for the Data dictionary into console output (with some pretty hackey indentation).
Now let's see how it all works.
Stepping all the way back to the beginning, lets see what happens now if we try to register a user with an invalid email address.
Change our registration values in Main()
back to the following:
string email = "john";
string password = "Password@123";
string confirmPassword = "Password@123";
Run the Web Api application once more. Once it has properly started, run the Console application with the modified registration values. The output to the console should look like this:
Register a User with Invalid Email Address:
An Error Occurred:
Status Code: BadRequest
Errors:
Email 'john' is invalid.
Similarly, if we use a valid email address, but password values which are both too short, and also do not match, we get the following output:
Register a User with Invalid Password:
An Error Occurred:
Status Code: BadRequest
Errors:
The Password must be at least 6 characters long.
The password and confirmation password do not match.
Finally, let's see what happens if we attempt to register the same user more than once.
Change the registration values to the following:
Using Valid Registration Values:
string email = "john@example.com";
string password = "Password@123";
string confirmPassword = "Password@123";
Now, run the console application twice in a row. The first time, the console output should be:
Console Output from Successful User Registration:
The new user john@example.com has been successfully added.
The next time, however, an error result is returned from our Web Api:
Console Output from Duplicate User Registration:
An Error Occurred:
Status Code: BadRequest
Errors:
Name john@example.com is already taken.. Email 'simon@example.com' is already taken.
Oh, yes I did . . . at least, in this case. This is a simple, console-based application in which nearly every result needs to end up as text output. Also, I'm just a rebel like that, I guess. Sometimes.
The important thing to realize is how to get the information we need out of the JSON which makes up the response content, and that is not as straightforward as it may seem in this case. How different errors are dealt with will, as always, need to be addressed within terms best suited for your application.
In a good many cases, treating Api errors as exceptions, to me, has merit. Doing so most likely will rub some architecture purists the wrong way (many of the errors incoming in response content don't really meet the textbook definition of "exception"). That said, for less complex .NET-based Api Client applications, unwrapping the errors from the response content, and throwing as exceptions to be caught by an appropriate handler can save on a lot of duplicate code, and provides a known mechanism for handling problems.
In other cases, or for your own purposes, you may choose to re-work the code above to pull out what you need from the incoming error response, but otherwise deal with the errors without using exceptions. Register() (and whatever other methods you use to call into your Api) might, in the case of a simple console application, return strings, ready for output. In this case, you could side-step the exception issue.
Needless to say, a good bit of the time, you will likely by calling into your Web Api application not from a desktop .NET application, but instead from a web client, probably using Ajax or something.
That's a Long and Crazy Post about Dealing with Errors - Wtf?
Well, I am building out a more complex, interactive console-based application in order to demo some concepts in upcoming posts. One of the more irritating aspects of that process was figuring out a reasonable way to deal with the various issues that may arise, when all one has to work with is a command line interface to report output to the user.
This was part of that solution (ok, in the application I'm building, things are a little more complex, a little more organized, and there's more to it. But here we saw some of the basics).
Well . . . YES!
In all likelihood, you just might tune up how and what you are pushing out to the client, depending upon the nature of your Web Api and the expected client use case. In this post, I went with the basic, default set-up (and really, we only looked at one method). But, depending upon how your Api will be used, you might very will handle errors and exceptions differently on the server side, which may impact how you handle things on the client side.