Skip to content

Beware the default WebAPI route with POST requests and a route parameter

July 12, 2012

The default route for WebAPI is similar to the default route for MVC. It looks like this:

public static void RegisterRoutes(RouteCollection routes)
{
    routes.MapHttpRoute(
        "DefaultApi",
        "api/{controller}/{id}",
        new { id = RouteParameter.Optional }
    );
}

And here’s what a typical WebAPI controller will look like:

public class MoviesController : ApiController
{
    public IEnumerable<Movie> Get() { ... }
    public HttpResponseMessage Get(int id)  { ... }
    public HttpResponseMessage Post(Movie movie)  { ... }
    public HttpResponseMessage Put(int id, Movie movie)  { ... }
    public HttpResponseMessage Delete(int id)  { ... }
}

The typical URLs this controller is expecting are:

GET ~/api/movies/
GET ~/api/movies/1
POST ~/api/movies/
PUT ~/api/movies/1
DELETE ~/api/movies/1

See the problem? I didn’t either until I was writing my C# client code and I had a bug in it. Here was the hastily written client code (hastily meaning I did a bunch of copy & paste):

public Task<MovieResponse> GetAsync(int id)
{
    var client = new HttpClient();
    var task = client.GetAsync(baseUrl + "/" + id, movie);
    ...
}
public Task<MovieResponse> PostAsync(Movie movie)
{
    var client = new HttpClient();
    var task = client.PostAsJsonAsync<Movie>(baseUrl + "/" + movie.ID, movie);
    ...
}
public Task<MovieResponse> PutAsync(Movie movie)
{
    var client = new HttpClient();
    var task = client.PostAsJsonAsync<Movie>(baseUrl + "/" + movie.ID, movie);
    ...
}

So see the problem now? The bug is that the movie ID is accidentally passed in the URL for the POST. The POST is expecting “~/api/movies/”, but in my bug above the URL was getting created as “~/api/movies/0” (the movie’s ID hadn’t yet been initialized and was 0). I discovered the bug when my server code was returning the wrong Location header for the 201 response:

public HttpResponseMessage Post(Movie movie)
{
    var newMovie = repository.Create(movie);
    var response = Request.CreateResponse(HttpStatusCode.Created, newMovie);
    var url = VirtualPathUtility.AppendTrailingSlash(Request.RequestUri.AbsoluteUri) + newMovie.ID;
    var uri = new Uri(url);
    response.Headers.Location = uri;
    return response;
}

And it was returning “~/api/movies/0/1” as the newly created resource’s URL, which was wrong. So I was very surprised that this incorrect request URL was making its way into my controller’s Post method, but knowing how routing works this should not be surprising at all. It’s certainly not expected, to say the least.

Arguably the bug is in the client code, but the server shouldn’t allow this. And for any WebAPI project with the default route this can happen.

So the issue is that the default route allows a POST when there is an id parameter. This is broken, IMO, so we need to fix it. To do so we’ll build a route constraint to prevent a POST when there’s an id parameter. Here’s the implementation of the route constraint:

public class ParamNotAllowedForMethod : IRouteConstraint
{
    string method;

    public ParamNotAllowedForMethod(string method)
    {
        this.method = method;
    }

    public bool Match(
        HttpContextBase httpContext, 
        Route route, 
        string parameterName, 
        RouteValueDictionary values, 
        RouteDirection routeDirection)
    {
        if (routeDirection == RouteDirection.IncomingRequest && 
            httpContext.Request.HttpMethod == method && 
            values[parameterName] != null)
        {
            return false;
        }

        return true;
    }
}

And he’s the updated route registration that uses the route constraint (the last parameter to MapHttpRoute). The key that’s used for the route constraint indicates which routing parameter to check and the constructor parameter indicates which method is not allowed.

public static void RegisterRoutes(RouteCollection routes)
{
    routes.MapHttpRoute(
        "DefaultApi",
        "api/{controller}/{id}",
        new { id = RouteParameter.Optional },
        new { id = new ParamNotAllowedForMethod("POST") }
    );
}

I will be using this route constraint for all WebAPI projects from now on.

5 Comments leave one →
  1. August 1, 2012 6:15 pm

    Great blog post, Brock! I too will be using this for all my Future WebApi projects.

  2. November 14, 2012 3:28 pm

    Brock . Did you observe that When returning false from the Match method basically it get calls two time. I tried with a new solution as well . Same behavior.

  3. tabishsarwar permalink
    November 14, 2012 3:31 pm

    I am using IHttpRouteConstraint though .

  4. tabishsarwar permalink
    November 14, 2012 3:35 pm

    Same happening with IRouteConstraint as well. sorry for posting comments like annoying texts !!!

  5. Nick D permalink
    March 24, 2017 4:46 pm

    This is why Attribute Routing is the only way to go…

Leave a comment