8

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?

Daniel T.
  • 33,336
  • 31
  • 125
  • 191

2 Answers2

8

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
  • 25,103
  • 3
  • 82
  • 100
  • Good answer, but shouldn't we use `Accept` instead of `ContentType`? http://stackoverflow.com/a/15898503/1804678 – Jess Oct 27 '14 at 19:14
  • @Jess the original question specifically mentioned `ContentType`. The `ContentType` indicates the format of the body of the request, whereas the `Accept` header indicates the *desired* formed of the body of the response. – Eilon Oct 28 '14 at 03:34
  • Any particular reason to default true when generating Urls? Thanks – Aleksander Bethke Nov 29 '18 at 20:55
  • 1
    @AleksanderBethke - it depends on the scenario, but from my experience, most people most of the time only wish to run constraint logic for incoming URLs. For URL generation they just want everything to match because the constraint is often matching on something that has no logical equivalent for URL generation (such as request content type headers). No wrong or right, just common patterns. – Eilon Nov 30 '18 at 04:07
4

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.

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.

Community
  • 1
  • 1
Dan Atkinson
  • 10,801
  • 12
  • 78
  • 106
  • @Jess It is. Sadly I took down my blog a few years ago, but the meat and bones of the answer is still valid (albeit quite old). – Dan Atkinson Oct 28 '14 at 16:57