Implementing CORS support in ASP.NET Web APIs – take 2


This post was written for the Beta version of the ASP.NET MVC 4. The updates needed to make them run in the latest bits (Release Candidate) are listed in this new post.

The code for this post is published in the MSDN Code Gallery.

Last post I showed one way to implement CORS support in the ASP.NET Web APIs. It was somewhat simple, and enabled requests from CORS-aware browsers to all resources exposed by the APIs. This is basically equivalent to the CrossDomainScriptAccessEnabled property in WCF HTTP endpoints (although that was for JSONP). We can do better, though. Instead of enabling support for all actions, we can choose which ones we want to support cross-domain requests for, so we can enable cross-domain requests to GET, PUT and POST, but not DELETE, for example. This post will show how this can be implemented in a fairly simple way with the ASP.NET Web API action selection and filters support.

The straightforward way to approach this problem (which is what I originally tried) was to simply have an action filter applied to the operations which I wanted to support CORS – similar to the code below.

  1. [EnableCors]
  2. public IEnumerable<string> Get()
  3. {
  4.     return allValues;
  5. }
  6.  
  7. // GET /api/values/5
  8. [EnableCors]
  9. public string Get(int id)
  10. {
  11.     // implementation ommitted
  12. }
  13.  
  14. // POST /api/values
  15. [EnableCors]
  16. public HttpResponseMessage Post(string value)
  17. {
  18.     // implementation ommitted
  19. }
  20.  
  21. // PUT /api/values/5
  22. [EnableCors]
  23. public void Put(int id, string value)
  24. {
  25.     // implementation ommitted
  26. }
  27.  
  28. // DELETE /api/values/5
  29. public void Delete(int id)
  30. {
  31.     // implementation ommitted
  32. }

The action filter is really small, and when I tried it for the first request (get all values), it worked perfectly. The code executes after the action returns; if the request had an “Origin” header, then we tag the response with an “Access-Control-Allow-Origin” for the value of that header, and it all works out.

  1. public class EnableCorsAttribute : ActionFilterAttribute
  2. {
  3.     const string Origin = “Origin”;
  4.     const string AccessControlAllowOrigin = “Access-Control-Allow-Origin”;
  5.  
  6.     public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
  7.     {
  8.         if (actionExecutedContext.Request.Headers.Contains(Origin))
  9.         {
  10.             string originHeader = actionExecutedContext.Request.Headers.GetValues(Origin).FirstOrDefault();
  11.             if (!string.IsNullOrEmpty(originHeader))
  12.             {
  13.                 actionExecutedContext.Result.Headers.Add(AccessControlAllowOrigin, originHeader);
  14.             }
  15.         }
  16.     }
  17. }

Then I tried adding a new value (POST) to the values list. And it failed – the browser showed an error, and the request didn’t make it to the operation and the action filter didn’t get executed. The problem was that for “unsafe” requests (such as POST, PUT and DELETE), the browser first sends a preflight request, a HTTP OPTIONS request (see last post for more information) asking what kind of CORS support the service has. But there are no routes which map OPTIONS requests to any actions, which causes the request to fail.

To solve this problem we can use a custom action selector, which will map preflight OPTIONS requests for incoming URIs which have already a route mapping to an action to a new HttpActionDescriptor, which will intercept those requests and return a response with the appropriate Access-Control-Allow headers if the action has the [CorsEnabled] attribute applied to it.

We can see the action selector below. If the request is a CORS preflight request (OPTIONS method, with an “Origin” header), then we’ll replace the request with the method requested by the preflight request (via the “Access-Control-Request-Method” header), then delegate to the default action selector to try to find if that request maps to any action. If such action exists, and if that action has the EnableCorsAttribute filter applied to it, then we’ll return our own action descriptor (PreflightActionDescriptor). Otherwise we’ll simply delegate the call back to the default action selector.

  1. public class CorsPreflightActionSelector : ApiControllerActionSelector
  2. {
  3.     public override HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)
  4.     {
  5.         HttpRequestMessage originalRequest = controllerContext.Request;
  6.         bool isCorsRequest = originalRequest.Headers.Contains(Origin);
  7.         if (originalRequest.Method == HttpMethod.Options && isCorsRequest)
  8.         {
  9.             string accessControlRequestMethod = originalRequest.Headers.GetValues(AccessControlRequestMethod).FirstOrDefault();
  10.             if (!string.IsNullOrEmpty(accessControlRequestMethod))
  11.             {
  12.                 HttpRequestMessage modifiedRequest = new HttpRequestMessage(
  13.                     new HttpMethod(accessControlRequestMethod),
  14.                     originalRequest.RequestUri);
  15.                 controllerContext.Request = modifiedRequest;
  16.                 HttpActionDescriptor actualDescriptor = base.SelectAction(controllerContext);
  17.                 controllerContext.Request = originalRequest;
  18.                 if (actualDescriptor != null)
  19.                 {
  20.                     if (actualDescriptor.GetFilters().OfType<EnableCorsAttribute>().Any())
  21.                     {
  22.                         return new PreflightActionDescriptor(actualDescriptor, accessControlRequestMethod);
  23.                     }
  24.                 }
  25.             }
  26.         }
  27.  
  28.         return base.SelectAction(controllerContext);
  29.     }
  30. }

The custom action descriptor wraps the original one, and delegates most of the operations to it. The only members which it will implement itself are the ReturnType property (we’ll return a HttpResponseMessage directly), and the Execute method. On the Execute we create the response just like we did on the message handler example: map the “Access-Control-Request-[Method/Headers]” from the request to the “Access-Control-Allow-[Methods/Headers]” in the response. Notice that since we’re delegating all calls to the original action, including the list of filters, we don’t need to add the “Access-Control-Allow-Origin” header, since it will be added by the filter itself.

  1. class PreflightActionDescriptor : HttpActionDescriptor
  2. {
  3.     HttpActionDescriptor originalAction;
  4.     string accessControlRequestMethod;
  5.  
  6.     public PreflightActionDescriptor(HttpActionDescriptor originalAction, string accessControlRequestMethod)
  7.     {
  8.         this.originalAction = originalAction;
  9.         this.accessControlRequestMethod = accessControlRequestMethod;
  10.     }
  11.  
  12.     public override string ActionName
  13.     {
  14.         get { return this.originalAction.ActionName; }
  15.     }
  16.  
  17.     public override object Execute(HttpControllerContext controllerContext, IDictionary<string, object> arguments)
  18.     {
  19.         HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.OK);
  20.         
  21.         // No need to add the Origin; this will be added by the action filter
  22.  
  23.         response.Headers.Add(AccessControlAllowMethods, this.accessControlRequestMethod);
  24.  
  25.         string requestedHeaders = string.Join(
  26.             “, “,
  27.             controllerContext.Request.Headers.GetValues(AccessControlRequestHeaders));
  28.  
  29.         if (!string.IsNullOrEmpty(requestedHeaders))
  30.         {
  31.             response.Headers.Add(AccessControlAllowHeaders, requestedHeaders);
  32.         }
  33.  
  34.         return response;
  35.     }
  36.  
  37.     public override ReadOnlyCollection<HttpParameterDescriptor> GetParameters()
  38.     {
  39.         return this.originalAction.GetParameters();
  40.     }
  41.  
  42.     public override Type ReturnType
  43.     {
  44.         get { return typeof(HttpResponseMessage); }
  45.     }
  46.  
  47.     public override ReadOnlyCollection<Filter> GetFilterPipeline()
  48.     {
  49.         return this.originalAction.GetFilterPipeline();
  50.     }
  51.  
  52.     public override IEnumerable<IFilter> GetFilters()
  53.     {
  54.         return this.originalAction.GetFilters();
  55.     }
  56.  
  57.     public override ReadOnlyCollection<T> GetCustomAttributes<T>()
  58.     {
  59.         return this.originalAction.GetCustomAttributes<T>();
  60.     }
  61. }

Now all that’s left is to hook up the action selector to the dispatcher. There are two ways to do that: either use the service resolver and set the CorsPreflightActionSelector as the implementer for the IHttpActionSelector interface (that would use it for all controllers in the application), or use the [HttpControllerConfiguration] attribute applied to the controller for which you want to enable the CORS preflight support, as shown below.

  1. [HttpControllerConfiguration(HttpActionSelector = typeof(CorsPreflightActionSelector))]
  2. public class ValuesController : ApiController
  3. {
  4.     //…
  5. }

That’s it. Now we can remove the message handler which was used for the previous sample, and the cross-domain calls should continue working just fine.

[Code in this post]

Comments (20)

  1. Anonymous says:

    Hi

    Great post!

    I know that this is just an example, but shouldn't some kind of validation of the originHeader take place in the EnableCorsAttribute.. to make sure that the request is coming from a "friendly" source?

  2. It really depends. If your service doesn't rely on any cookies for maintaining state, for example, or if it doesn't have any state at all, it should be fine for it to be accessed in a cross-domain way by any client. Remember, if someone wants to call your service and they know the address, they will – the cross-domain restriction is enforced by the browsers, so someone can simply create a C# program, for example, and send the request to your service.

  3. Anonymous says:

    Great post… but I'm having trouble getting it to work with chrome.  Also I downloaded your code package and i cannot get it to compile and run.  I have vs 2010 with all the latest updates and mvc 4.

    Any help?

  4. What error do you get? It should work with VS 2010 with SP1 – that's what I used to create it – and the ASP.NET MVC 4 Beta download.

  5. Anonymous says:

    Hey Carlos,

    First error is:

    System.Web.HttpException occurred

     Message=File does not exist.

     Source=System.Web

     ErrorCode=-2147467259

     WebEventCode=0

     StackTrace:

          at System.Web.StaticFileHandler.GetFileInfo(String virtualPathWithPathInfo, String physicalPath, HttpResponse response)

     InnerException:

    Second error is:

    Microsoft.CSharp.RuntimeBinder.RuntimeBinderException occurred

     Message='System.Dynamic.DynamicObject' does not contain a definition for 'Title'

     Source=Microsoft.CSharp

     StackTrace:

          at Microsoft.CSharp.RuntimeBinder.RuntimeBinderController.SubmitError(CError pError)

     InnerException:

    I may be doing something stupid… But I download a fair amount of examples and don't seem to have issues like this.

    Thanks for your help!

    D

  6. Hi Dave, those errors don't make sense to me either. Since you say that other projects are working, and there isn't much code in this sample, you can start with a new MVC4 / Web API project, then start adding the components to it one by one, testing them as you go (to see where it starts breaking). Those would be roughly the steps:

    • Create a new MVC4 / Web API project (using the VS Template). Build and run.
    • Create the Filters directory, then create the EnableCorsAttribute class on it (copying the code from the sample). Build and run (CORS shouldn't be applied at this point yet).

    • Create a Selectors directory, then create the CorsPreflightActionSelector class on it. As always, build and run (no runtime differences at this point)

    • Apply the EnableCors attributes to the actions.

    • Apply the [HttpControllerConfiguration(HttpActionSelector = typeof(CorsPreflightActionSelector))] attribute to the ValuesController class.

    If after all steps everything works, you can then compare the files you have in this new project with the ones you currently have, and that should show what the error is.

  7. Anonymous says:

    I can't get this working. I downloaded your code and tried it. It works only if the all call is from same project.  I am using IE. For Firefox, cross domain code works right of the gate without having to add anything to my ASP .NET project. Any ideas?

  8. The support for cross-domain calls on IE 9 (and earlier) isn't great. With IE10 (which comes on Win8) it should work better. One workaround for it to work on IE9 (and IE8 as well, I think) is to force jQuery to go to the xDomainRequest object internally by setting

    jQuery.support.cors = true;

    in the beginning of the JS code.

  9. Anonymous says:

    Thanks Carlos,

    I followed your directions and now have your example working with both chrome and ie (provided I add the $.support.cors = true; for ie).  I suspect it has something to do with your nuget config.  I can pack up mine and ship it to you if you want.

    I'm still having trouble getting mine to work.  Apparently it is getting stuck in my validation module trying to validate the "OPTIONS" request.

  10. Anonymous says:

    Hey Carlos,

    I got your example working.  And I got my code working… yay… thanks for your help.

    BUT… when I install my mvc app on a remote host, and access it through jquery ajax on my local machine, ie still works fine but chrome is giving me the ole' NETWORK_ERR: XMLHttpRequest Exception 101.  

    My guess is that I have to either set (or not) the 'Origin' header in my ajax client, or enable something else on the server.  If I should set the Origin header what should I set it to?

    Thanks for your help.

    Dave

  11. Anonymous says:

    Woohoo… got it all working with ie, ff, and chrome

    Turns out I did not have IIS6 configured all of the way… found this web site: haacked.com/…/asp.net-mvc-on-iis-6-walkthrough.aspx

    Anyway, let me know if you want my clean package zipped and sent.

    Dave

  12. Anonymous says:

    Thanks Carlos!  Works like a charm.  You da man!

  13. Anonymous says:

    Having issues now that I upgraded to the MVC RC.

    Have you tried the code with the new release?

  14. Anonymous says:

    Hey Carlos… still having trouble getting this to work on the latest release.  Looks like HttpActionDescriptor may have changed.

  15. Anonymous says:

    Do you have any updated source for the latest ASP .NET Web API RC?

  16. Anonymous says:

    The latest code is not working for Google Chrome and FireFox. why?

  17. Hi Dave, Jonathan, anonymous and Tanveer, I updated the code to work with the ASP.NET Web API Release Candidate version. I added a link on the top of this page for a post which explains the differences, or you can go directly to blogs.msdn.com/…/cors-support-in-asp-net-web-api-rc-version.aspx for the new post.

  18. Anonymous says:

    actionExecutedContext.Result.Headers.Add(AccessControlAllowOrigin, originHeader); should be actionExecutedContext.Request.Headers.Add(AccessControlAllowOrigin, originHeader);