Common no-cors misconceptions
Between tasks I spend a fair time on Stack Overflow, which is (truth be told) a love/hate relationship.
There’s a few recurring questions that I feel deserve a longer explanation.
Last time, I wrote about how to use MySQL in Node.js, but today I wanted
to write about the fetch no-cors
setting.
What is CORS?
Browsers have a ‘sandbox’. This sandbox prevents a script on some domain to do things on other domains.
For example, if a script runs on domain1.example.com
, it is not allowed to do
a PUT
request on domain2.example.org
. It also can’t embed an <iframe>
from domain2
and read its contents.
A script making a request to a different domain is very useful though, so workarounds were invented. Examples are server-side proxy scripts, using Flash and passing messages via iframe urls (and later on postMessage).
But then CORS came along, which offered a better solution. CORS allows
domain2
to say: “domain1
is allowed to make requests”.
The browser sandbox is not specific to exactly the domain. It’s actually the
combination of the scheme (http
or https
), the domain and the port. Those
3 parts combined are called the “origin”.
So, this is the first popular misconception: CORS does not add security, it selectively removes it. CORS is strictly opt-in, so if an origin does not have support for CORS headers, the old behavior stays in effect.
What is no-cors
?
If you’re trying to fetch()
to another origin, and this origin does not
opt into CORS, your request will return a network error and some messaging
in the console.
If you don’t control the API you are calling, you might be looking for
alternative solutions, and you might run into no-cors
:
const result = await fetch('https://domain2.example.org', {
method: 'POST',
mode: 'no-cors',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ foo: 'bar' }),
});
If you see the no-cors
setting, you might think that you are simply turning
off the CORS check, and when trying no-cors
it the first thing you might
notice is that the request now actually shows up on your network tab!
However, you will quickly run into new problems. no-cors
doesn’t just turn
off the check. It has greater consequences that make the request ‘safe’.
These are the main effects:
- Any request header except:
Accept
,Accept-Language
,Content-Language
,Content-Type
will be silently stripped from the request. - If
Content-Type
is something other thanapplication/x-www-form-urlencoded
,multipart/form-data
ortext/plain
, the header is ignored and set totext/plain
. - If the HTTP request is anything other than
GET
,HEAD
orPOST
, you will get an error. - You will not be able to read the response body, status or headers.
In other words, you can fire off a GET
, HEAD
or POST
request, but wont
know if it succeeded, failed, or what the response was, and if you POST
you can only really encode the request body as a HTML form, or text/plain
.
This makes the usefulness of no-cors
pretty limited to a few cases. In fact,
every case I’ve seen of people using no-cors
, it was a mistake.
This is the second misconception: no-cors
allows you to make certain cross-
origin requests, but it severely limits what you can do. For almost every case,
no-cors
is not what you want.
Why does no-cors
limit to specifically those features?
Before fetch and XMLHttpRequest, it was already possible to make cross-domain
requests with Javascript. The way to do this would be by creating a HTML form,
and calling .submit()
. With HTML forms it’s also not possible for Javascript
to see the response or use custom request bodies.
Because that feature existed before Javascript and before the need for
sandboxing, there was no need for an extra security layer to add this feature
to fetch()
. The world was already prepared for browsers doing these
requests.
The one exception to this is perhaps text/plain
requests, which (as far as I
know) was a newly lifted restriction that was deemed to not be risky.
So how do I fix my problem?
There’s 2 main solutions:
- Add CORS support to the API you are using. This only works if you have control over the target.
- Instead of making the request from your domain, something else needs to make the request for you.
If you can’t add CORS headers, you will likely want to build a small server-side script that can make these requests on your behalf.
Instead of calling the target directly, your script can now call your script, which has to do the request for you server-side.