How to Return Additional Info with HTTP Error Status Codes

December 30th 2013 HTTP ASP.NET Web API WCF

HTTP protocol defines status codes as the means of informing the client about different error conditions on the server during the request processing. Sometimes it can be beneficial to include additional information about the error that occurred. The protocol allows two different approaches in such cases:

  • The status code can be extended with an optional reason phrase which is intended for the user and not parsed by the clients. There are some limitations to it, though: it does not support different encodings and of course it can't span over multiple lines. While the latter usually can be avoided, the former one makes it impossible to return localized messages. Also, as I will describe in detail; depending on the API used, it might be impossible to retrieve reason phrase from code.
  • Even error pages can have content which doesn't manifest any of the above limitations: both different encodings and multi line content are supported. Unfortunately, based on the API used, it's again not always possible to retrieve the page content from code.

Let's take a more detailed look at different scenarios, starting with two generic client classes: HttpWebRequest and the newer HttpClient. On the server side I'll use Web API to return the desired response:

public class TestController : ApiController
{
    public string Get()
    {
        var response = new HttpResponseMessage(HttpStatusCode.InternalServerError);
        response.ReasonPhrase = "Database not available";
        response.Content = new StringContent("Database not available");
        throw new HttpResponseException(response);
    }
}

This will result in the following response:

HTTP/1.1 500 Database not available
Cache-Control: no-cache
Pragma: no-cache
Content-Length: 22
Content-Type: text/plain; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.5
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Sun, 29 Dec 2013 12:35:53 GMT

Database not available

Using HttpWebRequest the typical client code would look like:

var request = WebRequest.Create(uri);
var response = request.GetResponse();
using (var reader = new StreamReader(response.GetResponseStream()))
{
    var content = reader.ReadToEnd();
}

For error status codes response.GetResponseStream will throw a WebException which can be inspected as follows:

catch (WebException e)
{
    var message = e.Message;
    using (var reader = new StreamReader(e.Response.GetResponseStream()))
    {
        var content = reader.ReadToEnd();
    }
}

Message property will contain a generic status dependent error message (The remote server returned an error: (500) Internal Server Error.), while Response.GetResponseStream() returns a stream with the page content (Database not available in my case). Reason phrase can't be accessed in this case.

Using HttpClient this would be typical client code:

var httpClient = new HttpClient();
var result = await httpClient.GetStringAsync(uri);

For error status codes httpClient.GetStringAsync will throw a HttpRequestException with Message property containing Response status code does not indicate success: 500 (Database not available)., i.e. both the status code and the reason phrase. To retrieve the body of error responses, the client code needs to be modified:

var httpClient = new HttpClient();
var response = await httpClient.GetAsync(uri);
var content = await response.Content.ReadAsStringAsync();
response.EnsureSuccessStatusCode();

In this case content will contain page content for both non-error and error responses. StatusCode and ReasonPhrase properties of response can be used to get the corresponding response values. EnsureSuccessStatusCode will throw the same exception as GetStringAsync in the first sample code.

Web services should usually throw exceptions from web method code so that they are returned to the client inside the SOAP response. Still, sometimes the client needs to inspect other HTTP server responses as well. The simplest way to simulate this situation is to construct such a response from an IHttpModule:

public class ExceptionModule : IHttpModule
{
    public void Init(HttpApplication context)
    {
        context.AuthenticateRequest += ContextOnAuthenticateRequest;
    }

    private void ContextOnAuthenticateRequest(object sender, EventArgs eventArgs)
    {
        var response = HttpContext.Current.Response;
        response.StatusCode = (int)HttpStatusCode.InternalServerError;
        response.StatusDescription = "Database not available";
        response.Write("Database not available");
        response.Flush();
        response.Close();
    }

    public void Dispose()
    { }
}

Once the module is registered in web.config, the above response will be returned for any web service in the web application:

<configuration>
  <system.webServer>
    <modules>
      <add name="ExceptionModule" type="WebAPI.ExceptionModule" />
    </modules>
  </system.webServer>
</configuration>

After adding the web service to the client project as a service reference, the following (WCF) client code can be used:

var proxy = new WebServiceClient();
var result = proxy.WebMethod();

In case of an error status code the WebMethod will throw a MessageSecurityException, containing a WebException as InnerException. This WebException can be inspected just like when using HttpWebRequest.

Of course web services can still be added to the client project as an old school web reference. The client code stays similar:

var proxy = new WebService();
var result = proxy.WebMethod();

The behavior changes, though: WebMethod will throw an unwrapped WebException. Not only that; it will close the response stream returned by Response.GetResponseStream(), before returning, making it impossible to get the response body from code. Not so long ago I've spent too much time wondering why the stream is claimed to be disposed when I accessed it. Actually this has been the main reason, I started writing this blog post at all. Well, before concluding I should also mention that as a kind of a consolidation, the Message property in this case includes the reason phrase: The request failed with HTTP status 403: Database not available.

So, what is the best approach to returning additional information with error status codes? Obviously it depends on the scenario. As a general rule I would suggest, you always include a short non-localized reason phrase. To make sure it is accessible in all cases, it's a good idea to include it in the page content as well. If this message needs to be expanded further or localized, include that in the page content instead.

Copyright
Creative Commons License