Kalan's Blog

Software Engineer / Taiwanese / Life in Fukuoka

Current Theme light

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:

CORS Error Message

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:

  1. The request method must be GET, HEAD, or POST.
  2. Except for automatically set headers by the user-agent and specific headers, no additional headers are allowed. Acceptable headers.
  3. 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.

  1. You must add an OPTIONS endpoint that is the same as the API endpoint and set Access-Control-Allow-Origin to meet CORS requirements.
  2. 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)

Preflight Error Message

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:

Cookie Blocked Image

Cookie Not Written Image

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.

Cookie Sent Image

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.

Blocked Third-Party Cookies

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.

Reference Articles

Prev

Japanese software industry common nouns conversion

Next

New way to cancel requests - AbortController

If you found this article helpful, please consider buy me a drink ☕️ It'll make my ordinary day shine✨

Buy me a coffee

作者

Kalan 頭像照片,在淡水拍攝,淺藍背景

愷開 | Kalan

Hi, I'm Kai. I'm Taiwanese and moved to Japan in 2019 for work. Currently settled in Fukuoka. In addition to being familiar with frontend development, I also have experience in IoT, app development, backend, and electronics. Recently, I started playing electric guitar! Feel free to contact me via email for consultations or collaborations or music! I hope to connect with more people through this blog.