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]