How to Return Additional Info with HTTP Error Status Codes
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.