7

I am trying to implement HTTP method override following the steps described here. Basically, I am creating a DelegatingHandler, similar to the following, and adding it as a message handler on Application_Start.

public class MethodOverrideHandler : DelegatingHandler      
{
    readonly string[] _methods = { "DELETE", "HEAD", "PUT" };
    const string _header = "X-HTTP-Method-Override";

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // Check for HTTP POST with the X-HTTP-Method-Override header.
        if (request.Method == HttpMethod.Post && request.Headers.Contains(_header))
        {
            // Check if the header value is in our methods list.
            var method = request.Headers.GetValues(_header).FirstOrDefault();
            if (_methods.Contains(method, StringComparer.InvariantCultureIgnoreCase))
            {
                // Change the request method.
                request.Method = new HttpMethod(method);
            }
        }
        return base.SendAsync(request, cancellationToken);
    }
}

I have the following methods defined on my Controller:

  • persons/{id}, GET
  • persons/{id}, PUT
  • persons/{id}, DELETE

I can call them through their "native" methods and they work as expected. However, when I try to call them through a POST, sending the X-HTTP-Method-Override header with "DELETE" or "PUT", it gives a Not Found (404) error. It is important to add that, when it gives this error, it never reaches the MethodOverrideHandler -- I have put a Breakpoint which is never hit; it does hit the Breakpoint when I call normal DELETE and PUT.

I even tried adding another method:

  • persons/{id}, POST

When I do this, I get a Method Not Allowed (405) instead.

I thought that message handlers were run BEFORE the Routing and Controller dispatchers. Why is this giving me 404?

I do not think this is related, but I am not using default Web API routing. Instead, I am mapping using a custom Attribute, assigned to each method, like this:

routes.MapHttpRoute(
    String.Format("{0}_{1}", operation.Name, service.ServiceId),
    String.Format("{0}/{1}", service.RoutePrefix, routeTemplateAttribute.Template),
    defaults,
    new { httpMethod = GetHttpMethodConstraint(operation) });
[HttpDelete, RouteTemplate("persons/{id}")]
public HttpResponseMessage DeletePerson(string id)
{
    // ...
}

EDIT: GetHttpMethodConstraint code is below.

private static HttpMethodConstraint GetHttpMethodConstraint(MethodInfo methodInfo)
{
    var methodResolver = HttpMethodResolver.FromMethodInfo(methodInfo);
    return new HttpMethodConstraint(methodResolver.Resolve());
}
internal class HttpMethodResolver
{
    private MethodInfo _methodInfo;

    private HttpMethodResolver(MethodInfo methodInfo)
    {
        _methodInfo = methodInfo;
    }

    public static HttpMethodResolver FromMethodInfo(MethodInfo methodInfo)
    {
        return new HttpMethodResolver(methodInfo);
    }

    public string[] Resolve()
    {
        var verbs = new List<HttpMethod>();

        if (MethodHasAttribute<HttpGetAttribute>())
        {
            verbs.Add(HttpMethod.Get);
        }
        else if (MethodHasAttribute<HttpPostAttribute>())
        {
            verbs.Add(HttpMethod.Post);
        }
        else if (MethodHasAttribute<HttpDeleteAttribute>())
        {
            verbs.Add(HttpMethod.Delete);
        }
        else if (MethodHasAttribute<HttpPutAttribute>())
        {
            verbs.Add(HttpMethod.Put);
        }
        else
        {
            throw new ServiceModelException("HTTP method attribute should be used");
        }

        return verbs.Select(v => v.Method).ToArray();
    }

    private bool MethodHasAttribute<T>() where T : Attribute
    {
        return GetMethodAttribute<T>() != null;
    }

    private T GetMethodAttribute<T>() where T : Attribute
    {
        return _methodInfo.GetCustomAttributes(typeof(T), true).FirstOrDefault() as T;
    }
}
alextercete
  • 4,871
  • 3
  • 22
  • 36

2 Answers2

1

I think I'm having the same problem. It does look like the route constraints are checked before any message handlers.

So I created a custom constraint that knows to check for an overridden HTTP method:

class OverrideableHttpMethodConstraint : HttpMethodConstraint
{
    public OverrideableHttpMethodConstraint(HttpMethod httpMethod) : base(httpMethod)
    {
    }

    protected override bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection)
    {
        IEnumerable<string> headerValues;
        if (request.Method.Method.Equals("POST", StringComparison.OrdinalIgnoreCase) && 
            request.Headers.TryGetValues("X-HTTP-Method-Override", out headerValues))
        {
            var method = headerValues.FirstOrDefault();
            if (method != null)
            {
                request.Method = new HttpMethod(method);
            }
        }

        return base.Match(request, route, parameterName, values, routeDirection);
    }
}
Andrew Davey
  • 5,441
  • 3
  • 43
  • 57
0

I have tried to reproduce your error but I wasn't able to. Here, you can download my simple project with your message handler: https://dl.dropbox.com/u/20568014/WebApplication6.zip

I would like to point out that message handlers run before the action selection logic is performed. So, in your case, probably something else causes the problem and I think you should look at your other message handlers, your message handler's registration code, etc because the problem occurs due to the fact that your message handler never runs.

Also, I think your IRouteConstraint implementation, GetHttpMethodConstraint, looks suspicious to me.

Here is my registration code for the message handler:

protected void Application_Start(object sender, EventArgs e) {

    var config = GlobalConfiguration.Configuration;
    config.Routes.MapHttpRoute(
        "DefaultHttpRoute",
        "api/{controller}/{id}",
        new { id = RouteParameter.Optional }
    );

    config.MessageHandlers.Add(new MethodOverrideHandler());
}
tugberk
  • 57,477
  • 67
  • 243
  • 335
  • Thank you for your answer! I am pretty sure `GetHttpMethodConstraint` is working correctly, since I can call methods declared as "PUT" and "DELETE" with success using their "native" verbs. The problem only occurs when I call using POST and the method override header. I have tried to register the message handler before and after que routes mapping, and it did not work. I also removed the other message handler I had registered to see if it was causing the issue, but no luck. I can confirm that your application works, so it does not have to do with infrastructure or anything. – alextercete Oct 30 '12 at 17:21
  • @alextercete `GetHttpMethodConstraint` kicks in before the message handlers. My money is on that your problem is related to `GetHttpMethodConstraint`. – tugberk Oct 30 '12 at 17:24
  • I added the code for `GetHttpMethodConstraint` on the question. I think that there is nothing special about it... – alextercete Oct 30 '12 at 17:36
  • I am awarding you for your effort with the answer. However, since I still did not solve my issue, I am leaving it as unanswered. Thanks anyway! – alextercete Nov 04 '12 at 22:26