Introduction
CORS and cookies are important issues in front-end development. However, during development, these issues are often overlooked because the domain of the front-end and back-end is usually the same. Sometimes, developers simply request the back-end to set Access-Control-Allow-Origin: *
without fully understanding the underlying mechanism.
Regarding this issue, there is a comprehensive explanation on MDN. Therefore, this article aims to summarize the key points and common issues encountered in practical operations.
Same-Origin Policy
To prevent JavaScript from running rampant on web pages, the same-origin policy specifies that certain specific resources and code can only be accessed under the same origin.
So, what is the same origin? The origin of a document is defined by its protocol, host, and port. This means that if Document 1 comes from http://kalan.com
, and Document 2 comes from https://kalan.com
, they are not considered the same origin. What about subdomains? For example, https://api.foobar.com
and https://app.foobar.com
. Since their hosts are different, they are not considered the same origin either.
However, there are some resources that can be accessed across origins:
<img />
<video />
,<audio />
<iframe />
: Can be prevented from being embedded by defining headers- CSS scripts loaded via
<link rel="stylesheet" href />
- JavaScript loaded via
<script src="" />
On the other hand, cross-origin requests made through code (such as Fetch, XHR) are restricted by the same-origin policy.
Clearly, this policy is too strict. If everything is restricted by the same-origin policy, it would be very difficult to develop front-end and back-end applications, and it would also be impossible to use other SDK APIs through XHR. This is why the Cross-Origin Resource Sharing (CORS) mechanism was introduced.
Cross-Origin Resource Sharing (CORS)
Many people think that CORS is only relevant to front-end development. However, CORS usually requires the back-end to configure related headers and understand their meanings in order to work correctly.
So, how does cross-origin request work? It mainly relies on two headers: Origin
and Access-Control-Allow-Origin
.
As long as the value of the Origin
header in the request and the value of the Access-Control-Allow-Origin
header in the response are the same, or if Access-Control-Allow-Origin: *
is set (which means allowing any domain to access the resource), the request will be considered as fulfilling CORS requirements.
If the CORS requirements are not met, the following message will be displayed:
If you try to access the returned object, you will also receive a warning.
So... what happens if we follow the suggestion and change the fetch mode to no-cors
?
Indeed, we have eliminated the annoying error message, but the situation does not seem to improve.
no-cors
is not a magic solution. Even if this mode is used, CORS will not be bypassed, which means that your request will not be successfully sent. Therefore, the error SyntaxError: Unexpected end of input
occurs. This mode is usually used in conjunction with service workers.
From the experiment above, we can conclude that there is only one way to bypass CORS restrictions, which is to add the correct Control-Access-Allow-Origin
header on the server side (the host must be the same as the origin or set to *
).
In addition, the CORS mechanism only works when JavaScript sends XHR or fetch requests. Tools like curl or Postman do not have this mechanism, so developers often overlook this when testing API endpoints, resulting in inconsistencies between the front-end and back-end during API testing.
Some cross-origin requests do not require a preflight request, while others do. The conditions for triggering a preflight request are clearly explained on MDN:
- The request method must be GET, HEAD, or POST.
- Except for automatically set headers by the user-agent and specific headers, no additional headers are allowed. Acceptable headers.
- If
Content-Type
is present (note that this is a request header, not a response header), it must have one of the following values:application/x-www-form-encoded
,text/plain
,multipart/form-data
.
In other words, if the above conditions are not met, a preflight request will be sent.
Let's try changing the Content-Type
to application/json
to meet the requirements for a preflight request (not being application/x-www-form-encoded
, text/plain
, multipart/form-data
).
Preflight
The so-called preflight request is an HTTP OPTIONS request sent to another domain to confirm that there are no issues before sending the actual request. Once this condition is triggered, things become more complicated.
- You must add an OPTIONS endpoint that is the same as the API endpoint and set Access-Control-Allow-Origin to meet CORS requirements.
- You must add
Access-Control-Allow-Headers
and include all headers that are not covered by the conditions; otherwise, the request will not pass.
If the preflight check fails, you will receive the following error message:
Access to fetch at 'http://localhost:3001/trigger-preflight' from origin 'http://localhost:3000' has been blocked by CORS policy:
Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response.
Or, if you have not included Access-Control-Allow-Origin
in the response headers of the OPTIONS request:
Access to fetch at 'http://localhost:3001/trigger-preflight' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
If successful, you will see two requests in the network tab: one is OPTIONS and the other is the actual request.
What if we add a custom header? According to the requirements defined on MDN, it should trigger a preflight request. Let's add an X-Access-Token
header and see what happens.
fetch("http://localhost:3001/trigger-preflight", {
headers: { "X-Access-Token": "dontbeserious" },
})
.then(res => res.json())
.then(log)
Indeed, the preflight request cannot pass. To make it pass, you must add X-Access-Token
to Access-Control-Allow-Headers
.
Requests with Credentials
Cookies cannot be sent across domains, meaning that cookies from different origins cannot be accessed or shared. However, if you send a request from domain A to domain B and domain B returns a cookie, domain A will store a copy of the cookie as if it were from domain B. However, if withCredentials
or credentials: 'include'
is not set, even if the server returns Set-Cookie
, it will not be written. See the image below:
Under normal circumstances, cookies will not be automatically sent when using an API from domain B. In this case, you must set withCredentials
in XHR or { credentials: 'include' }
in the fetch options. Since this is also a cross-origin request, it must comply with CORS requirements and include Access-Control-Allow-Origin
.
fetch(`${hostname}/cookie`, {
method: "POST",
credentials: "include",
})
Access to fetch at 'http://localhost:3001/cookie' from origin 'http://localhost:3000' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.
For security reasons, browsers also specify that Access-Control-Allow-Origin
cannot be set to *
.
Access to fetch at 'http://localhost:3001/cookie' from origin 'http://localhost:3000' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'.
However, this is not enough. The browser automatically rejects responses without Access-Control-Allow-Credentials
. Therefore, if you want to send identity information to a server on a different domain, you must additionally include Access-Control-Allow-Credentials: true
. If everything is set correctly, you should see the cookie being successfully sent in the Request Cookies section.
Even if you have successfully configured all these settings, there is still a possibility that the cookie cannot be sent to the server. There are several possible scenarios for this:
1. Users have blocked cookies from this domain.
It is possible that users have blacklisted your domain, preventing the cookie from being sent successfully.
Solution:
- Change the domain
Reflect on why you were blacklisted by users
2. Users have blocked all third-party cookies.
Safari sometimes has this feature enabled, which can cause a lot of trouble during debugging.
Conclusion
Dealing with CORS is a thankless task, especially when you forget to add Access-Control-Allow-Origin or Access-Control-Allow-Credentials, and then have to go through the CI/CD and deployment process all over again. In this article, we have summarized some common issues and hope that in similar situations in the future, you will know how to handle them.
However, now we have AWS API Gateway, which can add the necessary headers without modifying the main code, or you can simply set up a proxy layer under the same domain to solve this issue.