Handling CORS Preflight requests to ASP.NET MVC actions

asp.net MvcCors

asp.net Mvc Problem Overview


I'm trying to perform a cross-domain POST request to an ASP.NET MVC controller action. This controller action accepts & uses various parameters. The problem is that when the preflight request happens, the controller action actually attempts to execute & because the OPTIONS request doesn't pass any data, the controller action throws out a 500 HTTP error. If I remove the code that uses the parameter, or the parameter itself, the entire request chain is completed successfully.

An example of how this is implemented:

Controller Action

public ActionResult GetData(string data)
{
	return new JsonResult
	{
		Data = data.ToUpper(),
		JsonRequestBehavior = JsonRequestBehavior.AllowGet
	};
}

Client-side code

<script type="text/javascript">
        $(function () {
            $("#button-request").click(function () {
                var ajaxConfig = {
                    dataType: "json",
                    url: "http://localhost:8100/host/getdata",
                    contentType: 'application/json',
                    data: JSON.stringify({ data: "A string of data" }),
                    type: "POST",
                    success: function (result) {
                        alert(result);
                    },
                    error: function (jqXHR, textStatus, errorThrown) {
                        alert('Error: Status: ' + textStatus + ', Message: ' + errorThrown);
                    }
                };

                $.ajax(ajaxConfig);
            });
        });
    </script>

Now, whenever the preflight request happens, it returns a 500 HTTP code, because the "data" parameter is null, seeing as the OPTIONS request doesn't pass any values.

The server application has been set up in my local IIS on port 8100 & the page running the client-side code is set up on port 8200 to mimic the cross-domain calls.

I have also configured the host (on 8100) with the following headers:

Access-Control-Allow-Headers: Content-Type
Access-Control-Allow-Methods: POST, GET
Access-Control-Allow-Origin: http://localhost:8200

One workaround I had found, was to check the HTTP method that executes the action & if it's a OPTIONS request to just return blank content, otherwise execute the action code. Like so:

public ActionResult GetData(string data)
{
	if (Request.HttpMethod == "OPTIONS") {
		return new ContentResult();
	} else {
		return new JsonResult
		{
			Data = data.ToUpper(),
			JsonRequestBehavior = JsonRequestBehavior.AllowGet
		};
	}
}

But this approach feels very clunky to me. I considered adding this sort of logic to an Attribute, but even this would mean decorating every action that will get called using CORS with it.

Is there a more elegant solution to getting this functionality to work?

asp.net Mvc Solutions


Solution 1 - asp.net Mvc

So I have found a solution that works. For each request, I check whether it's a CORS request & whether the request is coming in with the OPTIONS verb, indicating that it's the preflight request. If it is, I just send an empty response back (which only contains the headers configured in IIS of course), thus negating the controller action execution.

Then if the client confirms it's allowed to perform the request based on the returned headers from preflight, the actual POST is performed & the controller action is executed. And example of my code:

protected void Application_BeginRequest()
{
	if (Request.Headers.AllKeys.Contains("Origin", StringComparer.OrdinalIgnoreCase) &&
        Request.HttpMethod == "OPTIONS") {
		Response.Flush();
	}
}

As mentioned, this worked for me, but if anyone knows of a better way, or of any flaws in my current implementation, I would appreciate to hear about them.

Solution 2 - asp.net Mvc

expanding on Carl's answer, i took his code and plugged it into my OWIN pipeline:

app.Use((context, next) =>
{
     if (context.Request.Headers.Any(k => k.Key.Contains("Origin")) && context.Request.Method == "OPTIONS")
     {
         context.Response.StatusCode = 200;
         return context.Response.WriteAsync("handled");
     }

     return next.Invoke();
});

Just add this to the beginning (or anywhere before you register the WebAPI) of your IAppBuilder in Startup.cs

Solution 3 - asp.net Mvc

Here is how I handled the preflight/CORS issues with ASP.Net Web Api. I simply added the Microsoft.AspNet.WebApi.Cors Nuget package to my Web project.Then in my WebApiConfig.cs file I added this line:

config.EnableCors(new ApplicationCorsPolicy());

and created a custom PolicyProvider class

public class ApplicationCorsPolicy : Attribute, ICorsPolicyProvider
{
    public async Task<CorsPolicy> GetCorsPolicyAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var corsRequestContext = request.GetCorsRequestContext();
        var originRequested = corsRequestContext.Origin;

        if (await IsOriginFromAPaidCustomer(originRequested))
        {
            // Grant CORS request
            var policy = new CorsPolicy
            {
                AllowAnyHeader = true,
                AllowAnyMethod = true
            };
            policy.Origins.Add(originRequested);
            return policy;
        }
        // Reject CORS request
        return null;
    }

    private async Task<bool> IsOriginFromAPaidCustomer(string originRequested)
    {
        // Do database look up here to determine if origin should be allowed.
        // In my application I have a table that has a list of domains that are
        // allowed to make API requests to my service. This is validated here.
        return true;
    }
}

See, the Cors framework allows you to add your own logic for determining which origins are allowed, etc. This is very helpful if you are exposing a REST API to the outside world and the list of people (origins) who can access your site are in a controlled environment like a database. Now, if you are simply allowing all origins (which might not be such a good idea in all cases) you can just do this in WebApiConfig.cs to enable CORS globally:

config.EnableCors();

Just like Filters and Handlers in WebApi you can also add class or method level annotations to your controllers like so:

[EnableCors("*, *, *, *")]

Note that the EnableCors attribute has a constructor that accepts the following parameters

  1. List of Origins Allowed
  2. List of request headers allowed
  3. List of HTTP methods allowed
  4. List of response headers allowed

You can specify statically at each controller/end point who is allowed to access what resource.

Update 06/24/2016: I should mention that I have the following in my Web.config. It looks like these may not be the defaults for everyone.

<system.webServer>
    <handlers>
        <remove name="ExtensionlessUrlHandler-Integrated-4.0" />
        <remove name="OPTIONSVerbHandler" />
        <remove name="TRACEVerbHandler" />
        <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
        </handlers>
</system.webServer>

Source: Microsoft

Solution 4 - asp.net Mvc

The accepted answer works like a charm, but I found that the request was actually being passed down to the controller. I was receiving a 200 status code, but the response body contained a lot of HTML with an exception from the controller. So instead of using Response.Flush(), I found it was better to use Response.End(), which does stop the execution of the request. This alternative solution would look like this:

EDIT: fixed a typo carried from the original answer.

protected void Application_BeginRequest()
{
    if (Request.Headers.AllKeys.Contains("Origin", StringComparer.OrdinalIgnoreCase) &&
        Request.HttpMethod == "OPTIONS") {
        Response.End();
    }
}

Solution 5 - asp.net Mvc

This may be a red herring. I have recently got CORS working fine without jumping through any of the hoops that you are doing.

This was done using a combination of Thinktecture.IdentityModel nuget package, and more importantly... REMOVAL of all references to WebDAV. This includes removing the webdav module from IIS, and ensuring the following lines in your web config:

<system.webServer>
    <validation validateIntegratedModeConfiguration="false" />
    <modules runAllManagedModulesForAllRequests="true">
      <remove name="WebDAVModule" />
      <add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah" preCondition="managedHandler" />
      <add name="ErrorMail" type="Elmah.ErrorMailModule, Elmah" preCondition="managedHandler" />
      <add name="ErrorFilter" type="Elmah.ErrorFilterModule, Elmah" preCondition="managedHandler" />
    </modules>
    <handlers>
      <remove name="WebDAV" />
      <remove name="ExtensionlessUrlHandler-ISAPI-4.0_32bit" />
      <remove name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" />
      <remove name="ExtensionlessUrlHandler-Integrated-4.0" />
      <add name="ExtensionlessUrlHandler-ISAPI-4.0_32bit" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" />
  <add name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" />
  <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
</handlers>

Then you can just use thinktecture to configure your CORS from your Global.asax using a static class like this:

public class CorsConfig
{
    public static void RegisterCors(HttpConfiguration httpConfiguration)
    {
        var corsConfig = new WebApiCorsConfiguration();
        corsConfig.RegisterGlobal(httpConfiguration);

        corsConfig.ForAllResources().AllowAllOriginsAllMethodsAndAllRequestHeaders();
    }
}

SOURCE: http://brockallen.com/2012/06/28/cors-support-in-webapi-mvc-and-iis-with-thinktecture-identitymodel/

Solution 6 - asp.net Mvc

None of these answers worked for me, but the following webconfig settings did. The two key settings for me were setting Access-Control-Allow-Headers to Content-Type and commenting out the line that removes the OPTIONSVerbHandler:

  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true"></modules>
    <httpProtocol>
      <customHeaders>
        <add name="Access-Control-Allow-Origin" value="*" />
        <add name="Access-Control-Allow-Headers" value="Content-Type" />
      </customHeaders>
    </httpProtocol>
    <handlers>
      <remove name="ExtensionlessUrlHandler-Integrated-4.0" />
      <!--<remove name="OPTIONSVerbHandler" />-->
      <remove name="TRACEVerbHandler" />
      <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
    </handlers>
  </system.webServer>

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionCarl Heinrich HanckeView Question on Stackoverflow
Solution 1 - asp.net MvcCarl Heinrich HanckeView Answer on Stackoverflow
Solution 2 - asp.net MvcJonesopolisView Answer on Stackoverflow
Solution 3 - asp.net MvcRob LView Answer on Stackoverflow
Solution 4 - asp.net MvcGabriel DiéguezView Answer on Stackoverflow
Solution 5 - asp.net Mvcbeyond-codeView Answer on Stackoverflow
Solution 6 - asp.net MvcTreeAndLeafView Answer on Stackoverflow