Route matching and overriding 404 in ASP.NET Web API

In ASP.NET Web API, if the incoming does not match any route, the framework is simply hard wired to return 404 to the client (or possibly pass through to the next configured middleware, in case of an OWIN hosted Web API). This is done immediately, without entering anywhere further down the pipeline (i.e. message handlers would not be invoked).

However, an interesting question was posted recently at StackOverflow – what if you want to override that hard 404, and given your specific routing requirements, respond to the client with a different status code if a specific route condition fails?

I already answered at StackOverflow, but decided this deserves a blog post regardless.

Default behavior of route matching

By default, all incoming HTTP requests are handled by an HttpServer instance, which uses HttpRoutingDispatcher (a specialized HttpMessageHandler) to determine if there is a Web API route that matches the request.

If no match is found, HttpRoutingDispatcher will arbitrarily short circuit a 404 response without letting anything else in the Web API pipeline touch the request anymore.

Contrary to most Web API services, HttpRoutingDispatcher is not resolved from anywhere, but rather newed up directly right in the heart of the framework, so swapping it with alternative implementation (to changed the 404 behaviour) is pretty much impossible.

The problem

So if you visit that StackOverflow question, the OP wants to respond with 412 (Precondition Failed) instead of 404 if the route is matched, but his constraint is not fulfilled.

Implementing a smart IHttpRouteConstraint

To create custom constraining, you need to write a class that implements IHttpRouteConstraint. The interface is simple, as it only has a single method:

The principle is very straight forward – you have access to all the necessary information such as the route parameter name, its values supplied form the client side and the request and route information. Based on that, if your route pre-condition is matched, return true, otherwise, return false.

The trick is, that you can also throw an exception – and if it’s an HttpResponseException, the Web API infrastructure (the HttpServer) will catch it and convert it into an HttpResponseMessage (if the exception carries a relevant status code) or use the instance of the HttpResponseMessage attached to the exception itself.

The range constraint, with a custom status code, which addresses our problem, is shown below.

Note that in attribute routing, all constraint parameters have to be passed as primitives so the status code is passed as string and then cast to the HttpStatusCode enumeration. Alternatively, you can use an int there, since that would cast correctly too. The second constructor (with strongly typed status code) will be used in centralized routing.

If the route value falls outside of the expected range, we throw the Exception, instead of returning false and relying on HttpRoutingDispatcher to issue a 404. If the value is not an int then we do not throw, since the route is not matched correctly anyway. Thanks to this, we can drop the additional integer constraint – since our range will already require it to be an integer anyway.

Remember that this is a greedy constraint – if there is another route with similar structure it would not be reached anymore, as this constraint is going to force a specific response explicitly in case it’s not matched.

Applying the constraint to attribute routing

In order to use this with attribute routing you need to give the constraint a name in the ConstraintMap. This is that at the application startup, through DefaultInlineConstraintResolver.

Instead of simply saying:

You have to say:

With such setup, you can now modify the original route to:

Such setup ensures a 412 response in case the routing constraint fails.

Applying the constraint to centralized routing

So far we dealt with direct routing (attribute routing), but the same constraint works equally well with centralized routing. You just need to use the appropriate overload of the MapHttpRoute method.

In this case, we can use the other constructor of our RangeWithStatusRouteConstraint and pass the status code in the enum format directly

Be Sociable, Share!