Cross-Origin Resource Sharing Preflight Request Caching in Webapi and C#

Written by Sepp Wijnands on Tuesday 5 November 2013 in webapi

If you want to be able to make Cross-Domain JSON requests from a web client you are probably already familiar with CORS.

In broad terms, CORS is a mechanism that gives HTTP servers the ability to control whether a cross domain XHR request from a client is allowed or should be rejected.

A HTTP server can accept or reject a client on several different criteria, some of these are:

  • The origin from the client (the domain from which the request comes from, supplied by the Origin HTTP header)
  • The request method (POST, GET, etc...)
  • Additional HTTP request headers (X-ZUMO-APPLICATION, etc...)
  • Credentials such as cookies and/or user authentication

Depending on which magic combination of features you use (from the above list) in a request, the browser will first issue a so called Preflight Request to the server before putting your actual request through.

It does this for every request you make, which might not be what you want.

The Preflight Request

The preflight request is meant as a way for the client and server to agree on which features can be used, before the actual request is going to be made.

An example of a cross domain request, preceded by a successful preflight request:

CORS Preflight request network capture

Because the preflight request is send before the actual request, no sensitive (think authentication headers) or large amounts of data has travelled over the wire. And the request can still be cancelled/rejected by the server at will.

However, as stated above, in a standard configuration, it will fire this preflight request for every request you make (depending on your settings), no matter if you call the same api end point ten times in a row with the exact same parameters.

Luckily, the CORS standard specifies a way to allow the server and client to cache preflight requests by using the CORS header Access-Control-Max-Age.

Implementing a caching CORS handler for WebApi in C#

By creating a Message Handler for WebApi you can implement the CORS mechanism in very few steps. WebApi Message handlers are executed before any actual code of a WebApi controller runs. This is exactly what we want when implementing the Preflight Request and the CORS protocol itself.

Using the example provided by Carlos Figueira as a basis, it is relatively simple to add the necessary Access-Control-Max-Age header during the preflight request, as shown in the code listing below.

One important thing to note about the code below, is that it is a very liberal implementation of the CORS specification. More to the point: It effectively disables all security aspects provided by CORS, by always allowing any and all requests, no matter the origin.


public class CorsHandler : DelegatingHandler
{
    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";

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        bool isCorsRequest = request.Headers.Contains(Origin);
        bool isPreflightRequest = request.Method == HttpMethod.Options;
        if (isCorsRequest)
        {
            if (isPreflightRequest)
            {
                return Task.Factory.StartNew<HttpResponseMessage>(() =>
                {
                    HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.OK);
                    response.Headers.Add(AccessControlAllowOrigin, request.Headers.GetValues(Origin).First());

                    string accessControlRequestMethod = request.Headers.GetValues(AccessControlRequestMethod).FirstOrDefault();
                    if (accessControlRequestMethod != null)
                    {
                        response.Headers.Add(AccessControlAllowMethods, accessControlRequestMethod);
                    }

                    string requestedHeaders = string.Join(", ", request.Headers.GetValues(AccessControlRequestHeaders));
                    if (!string.IsNullOrEmpty(requestedHeaders))
                    {
                        response.Headers.Add(AccessControlAllowHeaders, requestedHeaders);
                    }

                    // Allow authentication & cookie headers
                    response.Headers.Add(AccessControlAllowCredentials, "true");

                    // Cache preflight request for a very long time
                    response.Headers.Add(AccessControlMaxAge, int.MaxValue.ToString());                     
                    return response;
                }, cancellationToken);
            }
            else
            {
                return base.SendAsync(request, cancellationToken).ContinueWith<HttpResponseMessage>(t =>
                {
                    HttpResponseMessage resp = t.Result;
                    resp.Headers.Add(AccessControlAllowOrigin, request.Headers.GetValues(Origin).First());

                    // Allow authentication & cookie headers
                    resp.Headers.Add(AccessControlAllowCredentials, "true");
                    return resp;
                });
            }
        }
        else
        {
            return base.SendAsync(request, cancellationToken);
        }
    }
}

To use the CORS handler in a WebAPI project, add it to the MessageHandlers collection when configuring WebApi (by default this is done in the WebApiConfig.Register() method)

public static void Register(HttpConfiguration config)
{
    config.Routes.MapHttpRoute(
        name: "DefaultApi",
         ...
    );
    config.MessageHandlers.Add(new CorsHandler());
}

An example preflight request handled by the above handler might look like:

Request Method: OPTIONS
Status Code: 200 OK
Request Headers:
    Accept: */*
    Access-Control-Request-Headers: accept, authorization
    Access-Control-Request-Method: POST
    Connection: keep-alive
    Host: x.azurewebsites.net
    Origin: http://localhost:8042
    Referer: http://localhost:8042/

Response Headers:
    Access-Control-Allow-Credentials: true
    Access-Control-Allow-Headers: accept, authorization
    Access-Control-Allow-Methods: POST
    Access-Control-Allow-Origin: http://localhost:8042
    Access-Control-Max-Age: 2147483647

...
X-AspNet-Version:4.0.30319
X-Powered-By:ASP.NET

Even though the above handler works well, and you can set the cache timeout to a very high value (in seconds), you are still dependant on the client's browser implementation of CORS to honor your settings.

Different browsers use different maximum values, as this excellent blog post by Monsur Hossain explains. Chrome, for example, has a limit of a maximum of five minutes. Firefox has a more reasonable (depending on your opinion) of 24 hours.

comments powered by Disqus

Since 2006 our products and services have helped hundreds of people optimize their daily business: