ASP.NET Core MVC

Observations about ASP.NET MVC Core and converting YetaWF from ASP.NET MVC5 to ASP.NET Core MVC. The new MVC6 has great new features. Getting there from an existing MVC 5 app is HARD!

AJAX GET and Query String Length

06/10/2017

While implementing Unified Page Sets (a.k.a. Single Page Site) in YetaWF, I decided to go with Google's recommendation to Use GET For AJAX Requests, in this case to swap portions of the page in and out.

Well, it worked for a while... But soon this happened:

HTTP Error 400. The size of the request headers is too long

AJAX GET requests will cram all the data (even JSON, XML) into the Url's query string. Technically, the headers don't really exceed any limits. There are some Web.config settings that can be changed to address Url, content and query string length:

<security>
    <requestFiltering allowDoubleEscaping="true">
      <requestLimits maxQueryString="2048000" maxUrl="2048000" maxAllowedContentLength="2048000" />
    </requestFiltering>
</security>
...
<system.web>
    <compilation debug="true" targetFramework="4.6" />
    <httpRuntime targetFramework="4.6" maxQueryStringLength="2048000" maxUrlLength="2048000" maxRequestLength="2048000" executionTimeout="3600" />
</system.web>

That helped a little. But still, things would stop working at approximately 16K of query string data sent.

The real culprit is http.sys which sits ahead of IIS and looks at all incoming data. That's where certain limits are enforced, which you can't override in Web.config.

There are some http.sys registry entries like MaxFieldLength and MaxRequestBytes that could be changed to "fix" that. However, that's generally not an option if you don't have full control over the servers where your code runs (like shared hosting or if your code is open source and could run anywhere, like YetaWF).

Switch To AJAX POST?

Sure. In most cases you can do that. And that's then end of it. However, in our case we couldn't, because the initial AJAX GET request would in turn call multiple controller actions which were all decorated with [HttpGet], meaning they require a GET request to be discovered by MVC's routing.

X-HTTP-Method-Override Header

The X-HTTP-Method-Override header could be used to specify GET even with an AJAX POST request.

The code would look something like this:

$.ajax({
    url: '/YetaWF_Core/PageContent/Show?' + uri.query(),
    type: 'POST',
    data: JSON.stringify(data),
    dataType: 'json',
    traditional: true,
    contentType: "application/json",
    processData: false,
    headers: {
        "X-HTTP-Method-Override": "GET" // server has to think this is a GET request so all actions that are invoked actually work
    },
    . . . 

MVC even has support for the X-HTTP-Method-Override header. Well, almost. It will only accept the override if it is not GET or POST. 

I'm not sure why. WHY?

Fortunately, there is an easy-ish fix for this if you don't mind a global edit of your entire code base. 

The AcceptVerbs, HttpGet, HttpPost, etc. attributes that decorate controller actions need to be replaced with a custom implementation.

I replaced AcceptVerbs with AllowHttp and added AllowGet (replacing HttpGet), AllowPost (replacing HttpPost), etc. The implementation of these attributes is quite trivial, but you will have to replace all instances of AcceptVerbs, HttpGet, HttpPost, etc. on your controller actions.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Web;
using System.Web.Mvc;

namespace YetaWF.Core.Controllers {

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
    public abstract class AllowHttpBase : ActionMethodSelectorAttribute {

        public AllowHttpBase() { }
        public abstract List<string> Methods { get; }

        public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo) {
            HttpRequestBase request = controllerContext.HttpContext.Request;
            foreach (string verb in Methods) {
                if (request.Headers["X-HTTP-Method-Override"] == verb || request.HttpMethod == verb)
                    return true;
            }
            return false;
        }
    }
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
    public class AllowHttp : AllowHttpBase {
        private List<string> Verbs { get; }
        public AllowHttp(params string[] verbs) { Verbs = verbs.ToList(); }
        public override List<string> Methods { get { return Verbs; } }
    }
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
    public class AllowGet : AllowHttpBase {
        public AllowGet() { }
        public override List<string> Methods { get { return new List<string> { "GET" }; } }
    }
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
    public class AllowPost : AllowHttpBase {
        public AllowPost() { }
        public override List<string> Methods { get { return new List<string> { "POST" }; } }
    }
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
    public class AllowPut : AllowHttpBase {
        public AllowPut() { }
        public override List<string> Methods { get { return new List<string> { "PUT" }; } }
    }
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
    public class AllowDelete : AllowHttpBase {
        public AllowDelete() { }
        public override List<string> Methods { get { return new List<string> { "DELETE" }; } }
    }
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
    public class AllowHead : AllowHttpBase {
        public AllowHead() { }
        public override List<string> Methods { get { return new List<string> { "HEAD" }; } }
    }
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
    public class AllowPatch : AllowHttpBase {
        public AllowPatch() { }
        public override List<string> Methods { get { return new List<string> { "PATCH" }; } }
    }
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
    public class AllowOptions : AllowHttpBase {
        public AllowOptions() { }
        public override List<string> Methods { get { return new List<string> { "OPTIONS" }; } }
    }
}

With these attributes, replacing the existing AcceptVerbs, HttpGet, etc. attributes, MVC routing will discover all actions when the X-HTTP-Method-Override header is used in AJAX requests.

No Comments

Be the first to add a comment!

Add New Comment

Complete this simple form to add a comment to this blog entry.