Since I first got into front-end development, a large portion of the projects I've taken on have used a front-end/back-end separation architecture, where the back end only provides APIs and the front end renders the actual pages based on those APIs. Personally, I think this is a pretty good model: the front end and back end are each responsible for their own modules, the division of labor is clear, and it also gives the front end more room to work with.
Unlike the old template-based approach, after front-end/back-end separation, most communication between the front end and back end is done by the front end actively initiating requests to the back end. And most of the front end's requests consist of Ajax, which is a very convenient way to fetch data. However, once Ajax runs into cross-origin issues, things get much more troublesome. This article mainly sorts out some of the cross-origin request problems I've encountered in project development, and of course it will also cover some background knowledge about cross-origin requests. PS: There's a little Easter egg at the end of the article 😄

Strictly speaking, cross-origin requests are not limited to Ajax cross-origin requests. Rather, for a page, as long as it requests resources from another domain, that process counts as a cross-origin request. For example, a page that includes resources from another domain src 's <img> tags, as well as other third-party CSS styles introduced into the page, and so on.
For img and CSS, cross-origin requests themselves do not pose much of a security problem, because these requests are all read-only requests and do not cause side effects to the source resource. But if a cross-origin request is sent from a script, since scripts are highly flexible, browsers, for security reasons, restrict their functionality according to the same-origin policy, so that under normal circumstances scripts can only request same-origin resources. If a page really needs to request resources from other websites via script, then it should work under the mechanism of Cross-Origin Resource Sharing (CORS).
Wait a moment, what is the same-origin policy?

Same-origin policy

For two pages (resources), as long as they satisfy the following three conditions, they are said to conform to the same-origin policy:
  1. Same protocol
  2. Same port
  3. Same domain
In addition,about:blank and javascript: inherit the origin of the page that loaded these resources.data: resources are different, and they themselves have an empty, secure context.
In addition, a subdomain can use JS to set document.domain to pass the same-origin policy. For example:
in the subdomain http://a.example.com/test.html 's page, by setting via JS document.domain='example.com' , then the current page and http://example.com/page.html comply with the same-origin policy.
Simply put, for the page http://www.example.com/page1.html , none of the following pages comply with the same-origin policy with it, and scripts cannot directly request these resources:
  • https://www.example.com/page1.html : Different protocol
  • http://www.example.com:81/page1.html : Different port
  • http://another.example.com/page1.html : Different domain
So, what exactly is CORS?

CORS (Cross-Origin Resource Sharing)

CORS essentially specifies a series of HTTP headers to determine whether a script can make a cross-origin request. Before looking at these request headers, let's first see what types of cross-origin requests there are.
There are two ways to make a request through a script: one is to make the request by creating an XMLHttpRequest, and the other is to make the request through the fetch API.
Generally speaking, cross-origin requests can be roughly divided into two types, one of which is called a simple request, which meets the following conditions:
  • The request method is GET、 POST、 HEAD one of these.
  • Apart from the request headers automatically added by the browser (such as Connection \ User-Agent etc.), only the following request headers are allowed:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type
  • Content-Type The value of the request header can only be application/x-www-form-urlencoded、 multipart/form-data、 text/plain one of these.
Conversely, if any one of the above three rules is violated, then it is not a simple cross-origin request. The difference between a non-simple cross-origin request and a simple cross-origin request is that, before the request is sent, the browser first sends a preflighted request to confirm with the server whether the request to be made next is allowed.

Preflight request

In actual project development, when using XHR or the fetch API to request interfaces, in many cases some additional special request headers are included, or special HTTP methods are used, such as PUT、DELETE etc. (commonly seen in RESTful interfaces). Because of the extra request headers or the use of special HTTP methods, the browser treats these requests as non-simple cross-origin requests, and will automatically send a preflight request before the actual request is sent, that is, an OPTIONS request.
The OPTIONS request sends the special HTTP request headers and HTTP request method used by the current cross-origin request to the server, such as Access-Control-Request-Method and Access-Control-Request-Headers . After receiving the OPTIONS request, the server returns the corresponding response headers. The browser then determines whether the cross-origin request is allowed based on the returned response headers. Only when the browser determines that the OPTIONS request has passed will the actual request be sent. The following is an example of the response headers and request headers of an OPTIONS request along with the actual GET request:
OPTIONS /api4 HTTP/1.1
Host: us1.serenader.me:3333
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Access-Control-Request-Method: PUT
Origin: http://us1.serenader.me:3334
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36
Accept: */*
Referer: http://us1.serenader.me:3334/
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4,fr;q=0.2
HTTP/1.1 200 OKX-Powered-By: Express
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET,POST,PUT,DELETE
Content-Type: text/html; charset=utf-8
Content-Length: 2
ETag: W/"2-REvLOj/Pg4kpbElGfyfh1g"
Date: Thu, 19 Jan 2017 15:21:15 GMT
Connection: keep-alive
PUT /api4 HTTP/1.1
Host: us1.serenader.me:3333
Connection: keep-alive
Content-Length: 0
Pragma: no-cache
Cache-Control: no-cache
Origin: http://us1.serenader.me:3334
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36
Accept: */*
Referer: http://us1.serenader.me:3334/
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4,fr;q=0.2
HTTP/1.1 200 OKX-Powered-By: Express
Access-Control-Allow-Origin: *
Content-Type: text/html; charset=utf-8
Content-Length: 2
ETag: W/"2-REvLOj/Pg4kpbElGfyfh1g"
Date: Thu, 19 Jan 2017 15:21:15 GMT
Connection: keep-alive
Now that we understand simple cross-origin requests and non-simple cross-origin requests that send preflight requests, let us look at exactly which HTTP headers determine the "fate" of these cross-origin requests.
To help readers better understand the role of these HTTP headers, I wrote a simple demo and open-sourced it on GitHub. Those interested can go to this link to view the code, or visit this online demo to preview the effect:http://us1.serenader.me:3334/. Remember to open the Chrome console after the page loads to view detailed request information.

Access-Control-Allow-Origin

Access-Control-Allow-Origin is a response header that specifies which domains' scripts are allowed to request the current resource.
Cross-origin requests (whether simple or non-simple) will include the Origin The request header is used to indicate which domain is currently making the request. At this point, the server-side response headers must include an Access-Control-Allow-Origin and the value matches Origin request header, only then can the cross-origin request possibly succeed. Otherwise, it will always fail.
Access-Control-Allow-Origin is the first threshold. The matching rules for its value are:
  • If its value is the wildcard * , then all domains are allowed to make cross-origin requests
  • If its value is a specific fixed domain, then only that domain is allowed to make cross-origin requests, and other domains will fail
  • If its value is a domain with a wildcard, such as *.example.com , then that domain and its subdomains are allowed to make cross-origin requests.
For details, you can watch the demo,demo-0 shows the case where a script requests an interface that has not been configured with cross-origin headers, and the request is intercepted by the browser:
demo-1 shows that the interface is configured with Access-Control-Allow-Origin response header, but it is not the domain of the script request, and at this point the browser will report this kind of error:
Only when the correct Access-Control-Allow-Origin response header is configured can the request receive the response normally, such asdemo-2. At this point, the request headers and response headers are:
GET /api2 HTTP/1.1
Host: us1.serenader.me:3333
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Origin: http://us1.serenader.me:3334
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36
Accept: */*
Referer: http://us1.serenader.me:3334/
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4,fr;q=0.2
HTTP/1.1 200 OKX-Powered-By: Express
Access-Control-Allow-Origin: *
Content-Type: text/html; charset=utf-8
Content-Length: 2
ETag: W/"2-REvLOj/Pg4kpbElGfyfh1g"
Date: Thu, 19 Jan 2017 15:03:33 GMT
Connection: keep-alive
For simple cross-origin requests, usually only passing Access-Control-Allow-Origin this response header is enough for the request to succeed (cases such as with cookies are not considered for now and will be discussed below). But when the request is not a simple cross-origin request, the situation is more complicated.

Access-Control-Allow-Headers

Access-Control-Allow-Headers is used to tell the browser which special request headers are allowed for the current interface. This HTTP header generally appears in the response headers of an OPTIONS request.
When a request sets a special request header and the requested interface has not been configured with the Access-Control-Allow-Headers response header, the following error will be reported, as shown in demo-3 shown:
The screenshot above shows that the request carries a X-Custom-Header request header, but the request fails at the preflight stage. To make the request complete successfully, you must configure the following in the response to the OPTIONS request: Access-Control-Allow-Headers: X-Custom-Header。

Access-Control-Allow-Methods

Similar to the previous HTTP header,Access-Control-Allow-Methods it tells the browser which HTTP methods are allowed to request the current endpoint. This HTTP header is usually only meaningful in the response header of an OPTIONS request. When this response header is not provided, the following error will be reported:
Similarly, the screenshot above fails at the preflight stage. To make the request execute successfully, you need to configure the response header as follows:Access-Control-Allow-Methods: GET,POST,PUT。

Access-Control-Max-Age

Due to the existence of the OPTIONS request, for a non-simple request, there will actually be two requests sent. This more or less wastes bandwidth, since this validation should only occur the first time. Once it passes validation, if the same endpoint is requested again within the following period of time, then the OPTIONS request actually does not need to be sent again.
Fortunately, there is a response header called Access-Control-Max-Age that can achieve this. This response header specifies how long after a request passes the preflight request it does not need to trigger preflight again. This reduces the actual number of requests and reduces wasted bandwidth.

Access-Control-Allow-Credentials

By default, any cross-origin request will not carry any credentials. These credentials include:
  • cookie
  • requests related to authentication
  • TLS client certificates
However, in most cases, we need the request to carry cookies, so we need to enable the cross-origin request withCredentialsoption.
If you want to manually enable cookie transmission, there are the following methods;
  • XHR: set it for the XHR object xhr.withCredentials = true 。
  • fetch: enable credentials in the passed-in parameter options fetch(url, { credentials: 'include' })
Once enabled, withCredentials afterwards, cookies will be added by default when the request is sent out.
However, in addition to manually enabling withCredentials on the front end, the server side also needs corresponding response header support for the request to succeed.
Access-Control-Allow-Credentials This response header indicates whether the currently requested resource allows credentials to be attached. The request only succeeds when its value is true; otherwise it will fail, and the failure content is as follows:
You can refer to demo-7to view the request headers and response headers.
In addition, **once the withCredentials option is enabled, the server-side Access-Control-Allow-Origin response header cannot be a wildcard; it can only be a fixed domain, otherwise the request will fail.** The specific error content is:
demo-8 and demo-9 respectively demonstrate the case where the response header is configured as a wildcard and the case where the response header is correctly configured as a specific domain when the request carries cookies.

Summary

In general, when a request is made in a script, the following situations occur:
  1. If the protocol, port, or domain of the requested resource is consistent with the address of the page making the request, then it conforms to the same-origin policy, and the request can be sent normally. Otherwise, it is called a cross-origin request and must follow the CORS mechanism.
  2. In all cross-origin requests, the server side must return the Access-Control-Allow-Origin response header, and its value must match the value of the Origin request header in the request. Only then can the request be allowed; otherwise the request will be intercepted by the browser.
  3. Cross-origin requests fall into two types: simple cross-origin requests and non-simple cross-origin requests. Before a non-simple cross-origin request is sent, the browser first sends a preflight request, that is, an OPTIONS request, to verify whether the server allows access for that request. Only when the OPTIONS request succeeds will the actual request continue to be sent. Otherwise, the request will fail at the OPTIONS stage, and the subsequent actual request will not be sent either.
  4. When a request carries special request headers, the response to the OPTIONS request returned by the server must include Access-Control-Allow-Headers response header, and its value must contain the names of the special request headers carried by the request. Only then will the request succeed; otherwise it will be intercepted by the browser.
  5. When a request uses a special HTTP method, the response to the OPTIONS request returned by the server must include Access-Control-Allow-Methods response header, and its value must contain the HTTP method currently being used. If this response header is absent, or the method currently being used is not among its values, the request will be intercepted by the browser.
  6. Because a non-simple request actually sends two requests each time a resource is fully requested, in order to reduce the number of OPTIONS requests sent and thereby reduce wasted bandwidth, the server can configure Access-Control-Max-Age to specify how long the browser can cache the OPTIONS request, so that after one request succeeds, the next request to the same endpoint does not need to send an OPTIONS request again.
  7. When a cross-origin request needs to carry identity credentials such as cookies, the withCredentials option must be manually enabled, and the server needs to configure Access-Control-Allow-Credentials response header; otherwise the request will not carry any identity credentials, or when there is no Access-Control-Allow-Credentials the request will be intercepted by the browser.
  8. When a request carries identity credentials, in addition to configuring Access-Control-Allow-Credentials response header,Access-Control-Allow-Origin the value of the response header cannot be a wildcard; it must be a specific domain name. Otherwise it will be intercepted by the browser.
Among the above 8 points, points 3 and 8 are worth noting.
The OPTIONS request is a key point that is relatively easy to overlook. When writing interfaces, some backend developers often only know to write into the interface's response headers Access-Control-Allow-Origin , without being aware of the existence of the OPTIONS request. In particular, the OPTIONS request is not sent with every cross-origin request, which leads some people to wonder why, even though what I sent was a GET request, what was actually sent was an OPTIONS request. And even if cross-origin permission is granted for the OPTIONS request, it is still very easy, due to the lack of the corresponding Access-Control-Allow-Headers or Access-Control-Allow-Methods response headers, for the request to still fail.
Point 8 is also a very important key point. If your interface needs to provide services to websites on multiple different domains, then your interface cannot use identity credentials such as cookies, since Access-Control-Allow-Origin cannot be set to a wildcard, which limits the objects that can use the interface.

Easter egg time

It was mentioned earlier that only non-simple requests trigger an OPTIONS request, and that satisfying a simple request requires only those three conditions. But the reality is not as perfect as imagined.
If you use XMLHttpRequest to implement file upload, then if within xhr.upload this object you add any event listener, an OPTIONS request will be triggered. Even if at this point the request itself satisfies the three conditions for a simple request. And once the event listener is removed, it no longer happens. For details, refer to demo-10、 demo-11、 demo-12
This "bug" was something I discovered by accident when I was writing uploader I discovered this library by accident. At the time, I thought it was a browser bug, but after some searching on Stack Overflow, I realized it was actually a hidden "feature" of the browser..
Turns out this is not a bug. The spec for XMLHttpRequest does mention that upload progress event handlers should cause the "force preflight" flag to be set. I was a bit confused when this was not specifically mentioned in the CORS spec, even though that spec does reference the existence of a "force preflight" flag.