how to handle "OPTIONS Method" in ASP.NET MVC

Viewed 24268

My Sencha Touch app is posting a form to my WebService, but instead of sending POST it's sending OPTIONS.

I'm reading a similar thread here, but I just don't know how to handle the OPTIONS method in my code.

I did try adding the [AllowAjax] attribute to my Action, however it doesn't seem to exist in MVC3.

OPTIONS /GetInTouch/CommunicateCard HTTP/1.1
Host: webservice.example.com
Referer: http://192.168.5.206/ Access-Control-Request-Method: POST
Origin: http://192.168.5.206
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.71 Safari/534.24
Access-Control-Request-Headers: X-Requested-With, Content-Type
Accept: /
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3

In my ActionMethod, I'm using the following code.

    public JsonpResult CommunicateCard(CommunicateCard communicateCard)
    {

        // Instantiate a new instance of MailMessage
        MailMessage mMailMessage = new MailMessage();

        // removed for security/brevity

        // Set the body of the mail message
        mMailMessage.Body = communicateCard.name; // THIS IS CURRENTLY BLANK :-(

        // removed for security/brevity
        mSmtpClient.Send(mMailMessage);

        // do server side validation on form input
        // if it's valid return true
        // else return false
        // currently returning NULL cuz I don't care at this point.
        return this.Jsonp(null);
    }
6 Answers

I tried all the answers here and none worked. I eventually realized that browsers will treat the pre-flight check as failed if it returns non 200. In my case, IIS was returning 404, even with the headers. This is because I had 2 attributes on my controller method - [HttpPost] and [HttpOptions]. Apparently, this is not a valid mechanism for expressing multiple verbs. I had to use this attribute instead: [AcceptVerbs(HttpVerbs.Options | HttpVerbs.Post)]

After struggling a lot, I found out the only way to handle CORS preflight request is to handle it with a pair of HttpModule and HttpHandler. Sending the required headers is not enough. You have to handle the OPTIONS request early and not allow it to reach your controllers, because it will fail there.

The only way that I could do this was with an HttpModule.

I followed this blog post:

http://geekswithblogs.net/abhijeetp/archive/2016/06/04/adding-cors-support-for-asp.net--webapi-the-no-hassle.aspx

To summarize the work, this is the code:

namespace WebAPI.Infrastructure
{
    using System;
    using System.Web;
    using System.Collections;
    using System.Net;
    public class CrossOriginModule : IHttpModule
    {
        public String ModuleName
        {
            get { return "CrossOriginModule"; }
        }

        public void Init(HttpApplication application)
        {
            application.BeginRequest += (new EventHandler(this.Application_BeginRequest));
        }

        private void Application_BeginRequest(Object source, EventArgs e)
        {
            HttpApplication application = (HttpApplication)source;
            HttpContext context = application.Context;
            CrossOriginHandler.AddCorsResponseHeaders(context);
        }

        public void Dispose()
        {
        }
    }

    public class CrossOriginHandler : IHttpHandler
    {
        #region Data Members
        const string OPTIONS = "OPTIONS";
        const string PUT = "PUT";
        const string POST = "POST";
        const string PATCH = "PATCH";
        static string[] AllowedVerbs = new[] { OPTIONS, PUT, POST, PATCH };
        const string Origin = "Origin";
        const string AccessControlRequestMethod = "Access-Control-Request-Method";
        const string AccessControlRequestHeaders = "Access-Control-Request-Headers";
        const string AccessControlAllowOrigin = "Access-Control-Allow-Origin";
        const string AccessControlAllowMethods = "Access-Control-Allow-Methods";
        const string AccessControlAllowHeaders = "Access-Control-Allow-Headers";
        const string AccessControlAllowCredentials = "Access-Control-Allow-Credentials";
        const string AccessControlMaxAge = "Access-Control-Max-Age";
        const string MaxAge = "86400";
        #endregion

        #region IHttpHandler Members
        public bool IsReusable
        {
            get { return true; }
        }

        public void ProcessRequest(HttpContext context)
        {
            switch (context.Request.HttpMethod.ToUpper())
            {
                //Cross-Origin preflight request
                case OPTIONS:
                    AddCorsResponseHeaders(context);
                    break;

                default:
                    break;
            }
        }
        #endregion

        #region Static Methods
        public static void AddCorsResponseHeaders(HttpContext context)
        {
            if (Array.Exists(AllowedVerbs, av => string.Compare(context.Request.HttpMethod, av, true) == 0))
            {
                var request = context.Request;
                var response = context.Response;
                var originArray = request.Headers.GetValues(Origin);
                var accessControlRequestMethodArray = request.Headers.GetValues(AccessControlRequestMethod);
                var accessControlRequestHeadersArray = request.Headers.GetValues(AccessControlRequestHeaders);
                if (originArray != null &&
                    originArray.Length > 0)
                    response.AddHeader(AccessControlAllowOrigin, originArray[0]);
                response.AddHeader(AccessControlAllowCredentials, bool.TrueString.ToLower());

                if (accessControlRequestMethodArray != null &&
                    accessControlRequestMethodArray.Length > 0)
                {
                    string accessControlRequestMethod = accessControlRequestMethodArray[0];
                    if (!string.IsNullOrEmpty(accessControlRequestMethod))
                    {
                        response.AddHeader(AccessControlAllowMethods, accessControlRequestMethod);
                    }
                }
                if (accessControlRequestHeadersArray != null &&
                    accessControlRequestHeadersArray.Length > 0)
                {
                    string requestedHeaders = string.Join(", ", accessControlRequestHeadersArray);
                    if (!string.IsNullOrEmpty(requestedHeaders))
                    {
                        response.AddHeader(AccessControlAllowHeaders, requestedHeaders);
                    }
                }
            }
            if (context.Request.HttpMethod == OPTIONS)
            {
                context.Response.AddHeader(AccessControlMaxAge, MaxAge);
                context.Response.StatusCode = (int)HttpStatusCode.OK;
                context.Response.End();
            }
        } 
        #endregion
    }
}

and add them to web.config:

<system.webServer>  
    <modules runAllManagedModulesForAllRequests="true">
      <remove name="WebDAVModule" />
      <add name="CrossOriginModule" preCondition="managedHandler" type="WebAPI.Infrastructure.CrossOriginModule, Your_Assembly_Name" />
    </modules>
    <handlers>
      <remove name="WebDAV"/>
      <remove name="OPTIONSVerbHandler"/>
      <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" />
      <add name="CrossOrigin" verb="OPTIONS" path="*" type="WebAPI.Infrastructure.CrossOriginHandler, Your_Assembly_Name" />
    </handlers>   
     <security>
       <authorization>
         <remove users="*" roles="" verbs=""/>
         <add accessType="Allow" users="*" verbs="GET,HEAD,POST,PUT,PATCH,DELETE,DEBUG"/>
       </authorization>
     <requestFiltering>
       <requestLimits maxAllowedContentLength="6000"/>
       <verbs>
         <remove verb="OPTIONS"/>
         <remove verb="PUT"/>
         <remove verb="PATCH"/>
         <remove verb="POST"/>         
         <remove verb="DELETE"/>
       </verbs>
     </requestFiltering>
   </security> 
  </system.webServer>

This works for Web API and MVC.

Related