Per request error detail policy in ASP.NET Web API

Β· 910 words Β· 5 minutes to read

ASP.NET Web API does a really good job at letting you control the amount of error details returned in the framework responses. And that is for both your own (“developer-generated”) exceptions, as well as the ones produced by the Web API itself.

However, this setting (IncludeErrorDetailPolicy) is global, and configured against the HttpConfiguration, making runtime manipulation of the error detail policy rather difficult.

However there is a trick you can use.

More after the jump.

Understanding IncludeErrorDetailPolicy πŸ”—

You can really easily control the verbosity of errors by setting IncludeErrorDetailPolicy to one of its values:

    • IncludeErrorDetailPolicy.Default
    • IncludeErrorDetailPolicy.LocalOnly
    • IncludeErrorDetailPolicy.Always
    • IncludeErrorDetailPolicy.Never

The last three are rather self descriptive, and the Default value will use LocalOnly in self host mode, and will look for custom errors directive in the web.config for web host; if it doesn’t find it it will use LocalOnly too.

The error policy is set again the HttpConfiguration object, for example:

var config = new HttpSelfHostConfiguration("http://localhost:999");  
config.Routes.MapHttpRoute(  
name: "DefaultApi",  
routeTemplate: "api/{controller}/{id}",  
defaults: new { id = RouteParameter.Optional }  
);  
var server = new HttpSelfHostServer(config);

config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;  

The problem with setting this against the configuration, is that it’s quite difficult to manipulate at runtime. The configuration is shared between the requests, as well as between controllers. If you try to modify it at runtime (nothing stops you), you’d modify it for all requests.

Sure, you could implement per-controller configuration using IControllerConfiguration, but even that doesn’t solve the problem as it’s only called once, and then the controller descriptor will be cached.

Suppose you have the following scenario - you would like to have a user authenticated as “admin” to get full error, and everyone else get a generic error without any stacktrace details. The sole nature of this requirement is the ability to modify error policy per request, based on the user identity.

Per request error detail policy πŸ”—

I previously mentioned that Web API will first look into custom errors of web.config, to determine it’s error detail policy. It does it by inspecting an entry in the request dictionary:

Lazy<bool> includeErrorDetail;  
if (request.Properties.TryGetValue<Lazy<bool>>(HttpPropertyKeys.IncludeErrorDetailKey, out includeErrorDetail)) {  
return includeErrorDetail.Value;  
}  

Even though this functionality was intended for web host, it actually does that check for self host too. This should already hint us that we could (ab)use this dictionary to modify error settings on a per-request basis.

Let’s go back to the hypothetical scenario - we’d like error details to be included for admins. For the sake of simplicity I will not go into security details (this is, after all, not a post about API security); we’ll just have an apikey - and if the value of the key is admin user is an admin, otherwise not.

This should be enough to illustrate the example:

public class ApiKeyHandler : DelegatingHandler  
{  
protected override Task<HttpResponseMessage> SendAsync(  
HttpRequestMessage request, CancellationToken cancellationToken)  
{  
var queryParams = request.RequestUri.ParseQueryString();  
var apikey = queryParams["apikey"];

if (apikey != null)  
{  
var roles = new List<string>();  
if (apikey == "admin")  
{  
roles.Add("admin");  
}

var identity = new GenericIdentity("user");  
var principal = new GenericPrincipal(identity, roles.ToArray());  
Thread.CurrentPrincipal = principal;  
}

return base.SendAsync(request, cancellationToken);  
}  
}  

So we have a simple handler that extracts API key from the request and authenticates the user - adding him an “admin” role if necessary.

Let’s extend this with the error detail policy hack:

if (apikey == "admin")  
{  
request.Properties[HttpPropertyKeys.IncludeErrorDetailKey] = new Lazy<bool>(() => true);  
roles.Add("admin");  
}  

This tricks the API into thinking that custom errors have been switched off (mind you, this works for both web and self host!). And if Web API encounters an error during the lifetime of this request, it will produce an error with the full stack trace. Note! for this to work, you have to leave IncludeErrorDetailsPolicy in your configuration unchanged!

Let’s add the handler to the configuration:

config.MessageHandlers.Add(new ApiKeyHandler());  

If we now add some faulty controller:

public class TestController : ApiController  
{  
public void Get()  
{  
throw new Exception("The world has ended!");  
}  
}  

We can request it and see that we get full error details as an admin:

This, however, is not yet exactly what we want - because you could request the same URL as a non admin, and also get full exception stack. The reason for this, is that if the error policy is Default and there is no explicit definition of error details inclusion or omission in the request proeprties (the mentioned HttpPropertyKeys.IncludeErrorDetailKey), Web API will default to LocalOnly, and since we execute from local, the stack shows up. Mind you, this only affects local execution, so if you deploy this to live environment, it will work correctly and only show errors for admins.

Anyway, let’s add explicit lack of error details for non-admins:

request.Properties[HttpPropertyKeys.IncludeErrorDetailKey] = new Lazy<bool>(() => false);

if (apikey != null)  
{  
var roles = new List<string>();  
if (apikey == "admin")  
{  
request.Properties[HttpPropertyKeys.IncludeErrorDetailKey] = new Lazy<bool>(() => true);  
roles.Add("admin");  
}

var identity = new GenericIdentity("user");  
var principal = new GenericPrincipal(identity, roles.ToArray());  
Thread.CurrentPrincipal = principal;  
}  

So now we explicitly default to errors off for non-admins. The result:

This works for any other object in the API pipeline. Let’s create, for example, a bad handler:

public class BadHandler : DelegatingHandler  
{  
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)  
{  
var result = await base.SendAsync(request, cancellationToken);  
throw new Exception("Something terrible happened");  
return result;  
}  
}  

As long as it’s registered after our handler setting error detail policy, the error will only appear for the administrator:

The same would apply for all Web API errors!

About


Hi! I'm Filip W., a cloud architect from ZΓΌrich πŸ‡¨πŸ‡­. I like Toronto Maple Leafs πŸ‡¨πŸ‡¦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁒󠁳󠁣󠁴󠁿.

You can find me on Github, on Mastodon and on Bluesky.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP