views:

170

answers:

2

I have a controller that only accepts a POST on this URL:

POST http://server/stores/123/products

The POST should be of content-type application/json, so this is what I have in my routing table:

routes.MapRoute(null,
                "stores/{storeId}/products",
                new { controller = "Store", action = "Save" },
                new {
                      httpMethod = new HttpMethodConstraint("POST"),
                      json = new JsonConstraint()
                    }
               );

Where JsonConstraint is:

public class JsonConstraint : IRouteConstraint
{
    public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
    {
        return httpContext.Request.ContentType == "application/json";
    }
}

When I use the route, I get a 405 Forbidden:

The HTTP verb POST used to access path '/stores/123/products' is not allowed

However, if I remove the json = new JsonConstraint() constraint, it works fine. Does anybody know what I'm doing wrong?

+1  A: 

I would debug the JsonConstraint and see what the content type is.

It's possible that, for whatever reason, it may not be application/json.

I know that that is the RFC MIME type, but I've seen a few others floating around in my time (such as text/x-json), as has been mentioned here in a previous question.

Also, I've never seen a ContentType constraint, so I'd be interested to see if it works. Have you tried it with other MIME types just in case it's faulty?

And finally, rather than have just a single JsonConstraint, I'd create a generic ContentTypeConstraint.

Update:

I knocked together a quick WebRequest method on a route that uses the ContentTypeConstraint code, and that seems to work correctly. I also blogged about it here because it's an interesting idea, and not one I've thought of before!

Enum

public enum ConstraintContentType
{
  XML,
  JSON,
}

Constraint class

public class ContentTypeConstraint : IRouteConstraint
{
  private string mimeType;

  public ContentTypeConstraint(ConstraintContentType constraintType)
  {
    //FYI: All this code could be redone if you used the Description attribute, and a ToDescription() method.
    switch (constraintType)
    {
      case ConstraintContentType.JSON:
        mimeType = "application/json";
        break;
      case ConstraintContentType.XML:
        mimeType = "text/xml";
        break;
      default:
        mimeType = "text/html";
        break;
    }
  }

  public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
  {
    //As suggested by Eilon
    if (routeDirection == RouteDirection.UrlGeneration)
      return true;

    return httpContext.Request.ContentType == mimeType;
  }

This would be called, using your example, as:

contentType = new ContentTypeConstraint(ConstraintContentType.JSON)

This was the constraint is reusable for much more than just JSON. Also, the switch case can be done away with if you use description attributes on the enum class.

Dan Atkinson
+3  A: 

I'd put this in a comment but there isn't enough space.

When writing a custom constraint it is very important to inspect the routeDirection parameter and make sure that your logic runs only at the right time.

That parameter tells you whether your constraint is being run while processing an incoming request or being run while someone is generating a URL (such as when they call Html.ActionLink).

In your case I think you want to put all your matching code in a giant "if":

public bool Match(HttpContextBase httpContext, Route route,
    string parameterName, RouteValueDictionary values,
    RouteDirection routeDirection) 
{
    if (routeDirection == RouteDirection.IncomingRequest) {
        // Only check the content type for incoming requests
        return httpContext.Request.ContentType == mimeType; 
    }
    else {
        // Always match when generating URLs
        return true;
    }
}
Eilon