AngularJS performs an OPTIONS HTTP request for a cross-origin resource

I'm trying to setup AngularJS to communicate with a cross-origin resource where the asset host which delivers my template files is on a different domain and therefore the XHR request that angular performs must be cross-domain. I've added the appropriate CORS header to my server for the HTTP request to make this work, but it doesn't seem to work. The problem is that when I inspect the HTTP requests in my browser (chrome) the request sent to the asset file is an OPTIONS request (it should be a GET request).

I'm not sure whether this is a bug in AngularJS or if I need to configure something. From what I understand the XHR wrapper can't make an OPTIONS HTTP request so it looks like the browser is trying to figure out if is "allowed" to download the asset first before it performs the GET request. If this is the case, then do I need to set the CORS header (Access-Control-Allow-Origin: http://asset.host...) with the asset host as well?

OPTION request are by no means AngularJS bug, this is how Cross-Origin Resource Sharing standard mandates browsers to behave. Please refer to this document: https://developer.mozilla.org/en-US/docs/HTTP_access_control, where in the "Overview" section it says:

The Cross-Origin Resource Sharing standard works by adding new HTTP headers that allow servers to describe the set of origins that are permitted to read that information using a web browser. Additionally, for HTTP request methods that can cause side-effects on user data (in particular, for HTTP methods other than GET, or for POST usage with certain MIME types), the specification mandates that browsers "preflight" the request, soliciting supported methods from the server with an HTTP OPTIONS request header, and then, upon "approval" from the server, sending the actual request with the actual HTTP request method. Servers can also notify clients whether "credentials" (including Cookies and HTTP Authentication data) should be sent with requests.

It is very hard to provide a generic solution that would work for all the WWW servers as setup will vary depending on the server itself and HTTP verbs that you intend to support. I would encourage you to get over this excellent article (http://www.html5rocks.com/en/tutorials/cors/) that has much more details on the exact headers that needs to be sent by a server.

For Angular 1.2.0rc1+ you need to add a resourceUrlWhitelist.

1.2: release version they added a escapeForRegexp function so you no longer have to escape the strings. You can just add the url directly

'http://sub*.assets.example.com/**' 

make sure to add ** for sub folders. Here is a working jsbin for 1.2: http://jsbin.com/olavok/145/edit


1.2.0rc: If you are still on a rc version, the Angular 1.2.0rc1 the solution looks like:

.config(['$sceDelegateProvider', function($sceDelegateProvider) {
     $sceDelegateProvider.resourceUrlWhitelist(['self', /^https?:\/\/(cdn\.)?yourdomain.com/]);
 }])

Here is a jsbin example where it works for 1.2.0rc1: http://jsbin.com/olavok/144/edit


Pre 1.2: For older versions (ref http://better-inter.net/enabling-cors-in-angular-js/) you need to add the following 2 lines to your config:

$httpProvider.defaults.useXDomain = true;
delete $httpProvider.defaults.headers.common['X-Requested-With'];

Here is a jsbin example where it works for pre 1.2 versions: http://jsbin.com/olavok/11/edit

It's also possible to override the OPTIONS request (was only tested in Chrome):

app.config(['$httpProvider', function ($httpProvider) {
  //Reset headers to avoid OPTIONS request (aka preflight)
  $httpProvider.defaults.headers.common = {};
  $httpProvider.defaults.headers.post = {};
  $httpProvider.defaults.headers.put = {};
  $httpProvider.defaults.headers.patch = {};
}]);

Your service must answer an OPTIONS request with headers like these:

Access-Control-Allow-Origin: [the same origin from the request]
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: [the same ACCESS-CONTROL-REQUEST-HEADERS from request]

Here is a good doc: http://www.html5rocks.com/en/tutorials/cors/#toc-adding-cors-support-to-the-server

The same document says

Unlike simple requests (discussed above), "preflighted" requests first send an HTTP OPTIONS request header to the resource on the other domain, in order to determine whether the actual request is safe to send. Cross-site requests are preflighted like this since they may have implications to user data. In particular, a request is preflighted if:

It uses methods other than GET or POST. Also, if POST is used to send request data with a Content-Type other than application/x-www-form-urlencoded, multipart/form-data, or text/plain, e.g. if the POST request sends an XML payload to the server using application/xml or text/xml, then the request is preflighted.

It sets custom headers in the request (e.g. the request uses a header such as X-PINGOTHER)

When the original request is Get with no custom headers, the browser should not make Options request which it does now. The problem is it generates a header X-Requested-With which forces the Options request. See https://github.com/angular/angular.js/pull/1454 on how to remove this header

Perfectly described in pkozlowski's comment. I had working solution with AngularJS 1.2.6 and ASP.NET Web Api but when I had upgraded AngularJS to 1.3.3 then requests failed.

  • Solution for Web Api server was to add handling of the OPTIONS requests at the beginning of configuration method (more info in this blog post):

    app.Use(async (context, next) =>
    {
        IOwinRequest req = context.Request;
        IOwinResponse res = context.Response;
        if (req.Path.StartsWithSegments(new PathString("/Token")))
        {
            var origin = req.Headers.Get("Origin");
            if (!string.IsNullOrEmpty(origin))
            {
                res.Headers.Set("Access-Control-Allow-Origin", origin);
            }
            if (req.Method == "OPTIONS")
            {
                res.StatusCode = 200;
                res.Headers.AppendCommaSeparatedValues("Access-Control-Allow-Methods", "GET", "POST");
                res.Headers.AppendCommaSeparatedValues("Access-Control-Allow-Headers", "authorization", "content-type");
                return;
            }
        }
        await next();
    });
    

If you are using Jersey for REST API's you can do as below

You don't have to change your webservices implementation.

I will explain for Jersey 2.x

1) First add a ResponseFilter as shown below

import java.io.IOException;

import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;

public class CorsResponseFilter implements ContainerResponseFilter {

@Override
public void filter(ContainerRequestContext requestContext,   ContainerResponseContext responseContext)
    throws IOException {
        responseContext.getHeaders().add("Access-Control-Allow-Origin","*");
        responseContext.getHeaders().add("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT");

  }
}

2) then in the web.xml , in the jersey servlet declaration add the below

    <init-param>
        <param-name>jersey.config.server.provider.classnames</param-name>
        <param-value>YOUR PACKAGE.CorsResponseFilter</param-value>
    </init-param>