Thursday 19 April 2012

ASP.NET MVC4 Web API / appharbor / IIS / http status codes

So you've got your nice asp.net webapi app running locally, and its ready to deploy.
You're being a good RESTful developer, and using http status codes correctly.
You're sending back 400 BadRequest when validating user input, you're using 403 when somebody does something their not allowed to do, and 401 to issue authentication required.
You're sending nice friendly, informative error messages for those 4xx responses.

And then you deploy to appharbor, which is running on IIS 7, and you're app stops sending back those nice formatted error messages.

Weird, you think ... works locally !

You spend hours trying to figure it out. And then you finally ask @appharbor, and they let you know about a crazy HttpContext setting.

What it is, is IIS7 being mental. And stomping on your response body.
What you need to do is for every response that is not 2xx or 3xx, you need to do this :

HttpContext.Current.Response.TrySkipIisCustomErrors = true;

*** UPDATE ***
Khalid (see comments below) has pointed out that you can do this in Web.config instead, which is a better. Cheers fella ! See http://blog.aquabirdconsulting.com/?p=359
Example:

<system.webserver>
<httpErrors existingResponse="PassThrough"/>
</system.webserver>

*** END UPDATE ***

But where to put it ? Well, whereever works for you.
I follow this pattern in all my controllers :


public class ExampleController : ApiController
{
public HttpResponseMessage Delete(long id)
{
return this.Try(() =>
{
_organisationService.Delete(id);
return 204.Response();
});
}
}


Where 'Try' is a method that catches known exceptions (ie can't find an entity, input is invalid etc etc)


public static HttpResponseMessage Try(this ApiController controller, Func<HttpResponseMessage> operation)
{
try
{
return operation.Invoke();
}
catch (EntityNotFoundException e)
{
return 404.Response(MessageResponse.From(e.Message));
}
catch (ForbiddenException e)
{
return 403.Response(MessageResponse.From(e.Message));
}
catch(PermissionsException e)
{
return 403.Response(MessageResponse.From(e.Message));
}
catch (ValidationException e)
{
if (e.HasMultipleErrors())
{
return 400.Response(e.Errors);
}
return 400.Response(MessageResponse.From(e.Message));
}
catch (Exception e)
{
Debug.WriteLine(e.ToString());
return 500.Response(MessageResponse.From(e.ToString()));
}
}



and I have this extension method, where I put in the magic IIS hack :


public static HttpResponseMessage<T> Response<T>(this int code, T content)
{
HttpContext.Current.Response.TrySkipIisCustomErrors = true;
switch (code)
{
case 200:
return new HttpResponseMessage<T>(content, HttpStatusCode.OK);
case 201:
return new HttpResponseMessage<T>(content, HttpStatusCode.Created);
case 204:
return new HttpResponseMessage<T>(content, HttpStatusCode.NoContent);
case 400:
return new HttpResponseMessage<T>(content, HttpStatusCode.BadRequest);
case 401:
return new HttpResponseMessage<T>(content, HttpStatusCode.Unauthorized);
case 403:
return new HttpResponseMessage<T>(content, HttpStatusCode.Forbidden);
case 404:
return new HttpResponseMessage<T>(content, HttpStatusCode.NotFound);
case 500:
return new HttpResponseMessage<T>(content, HttpStatusCode.InternalServerError);
default:
throw new Exception(string.Format("Do not understand http code {0}", code));
}
}