CORS (Cross Origin Resource Sharing)

2022-12-16

This note is to help our customers understand CORS (especially as it relates to Infura). We’ll talk about how it works and what to do about your CORS errors.

References:

These are some of the best resources I’ve found:

https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS

https://medium.com/@baphemot/understanding-cors-18ad6b478e2b

https://en.wikipedia.org/wiki/Cross-origin_resource_sharing

Recommended:

https://blog.container-solutions.com/a-guide-to-solving-those-mystifying-cors-issues

What is CORS

It is a mechanism that allows restricted resources on a server to be requested from a webpage that was not served by that server, i.e. a webpage that was served by a different origin. It is enforced by the browser, which provides headers in the request (mainly the Origin header) so that the cross-site server can determine if it should authorise the browser to access the requested resource.

CORS is implemented in browsers. It’s relevant to browser based javascript, not to backend nodejs (or python or ruby) code. While you can do CORS experiments with curl this does NOT simulate the rest of the browser behaviour, esp. that of not returning the data from the server back to the JavaScript code that made the request - the browser quarantines the data if everything is not right. Nor does using curl show the error message, that you would see in the console, that the browser generates if the response from the server is not correct - you must interpret the headers yourself. If the request “works” in curl or postman that’s no guarantee that it will work from a browser.

Some requests are “simple” - GET, HEAD, and some POSTs, i.e. those that have “allowed” content types. The allowed POST content types are either are harmless (text/plain), or could be harmful but are legacy types that everyone knows how to deal with already (multipart/form-data).

However, even for the simple requests above, the browser will need to send a preflight if there’s authentication or other headers are present that are not on the CORS-safelisted list. For simple requests, that don’t include any headers not on the safelist, the browser doesn’t need to run a “preflight” request.

A preflight request is one with the verb “OPTIONS” plus some headers to ask the server for a response that says if the real request will be allowed.

Here’s an example pre-flight request copied from the browser devtools and represented as a curl command:

curl 'https://ipfs.infura.io:5001/api/v0/add?stream-channels=true&progress=false' \
  -X 'OPTIONS' \
  -H 'Accept: */*' \
  -H 'Accept-Language: en-AU,en-GB-oxendict;q=0.9,en;q=0.8' \
  -H 'Access-Control-Request-Headers: authorization' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Connection: keep-alive' \
  -H 'Origin: http://localhost:3000' \
  -H 'Referer: http://localhost:3000/' \
  -H 'Sec-Fetch-Dest: empty' \
  -H 'Sec-Fetch-Mode: cors' \
  -H 'Sec-Fetch-Site: cross-site' \
  -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36' \
  --compressed -v

And here is the corresponding actual request:

curl 'https://ipfs.infura.io:5001/api/v0/add?stream-channels=true&progress=false' \
  -H 'Accept: */*' \
  -H 'Accept-Language: en-AU,en' \
  -H 'Connection: keep-alive' \
  -H 'Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryFUZB7uL4BhiUxKJW' \
  -H 'Origin: http://localhost:3000' \
  -H 'Referer: http://localhost:3000/' \
  -H 'Sec-Fetch-Dest: empty' \
  -H 'Sec-Fetch-Mode: cors' \
  -H 'Sec-Fetch-Site: cross-site' \
  -H 'Sec-GPC: 1' \
  -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36' \
  -H 'authorization: Basic xxxtokenxxx' \
  --data-raw $'------WebKitFormBoundaryFUZB7uL4BhiUxKJW\r\nContent-Disposition: form-data; name="file"; filename=""\r\nContent-Type: application/octet-stream\r\n\r\nxxxxBINARY_DATA_bla_blaxxxx'
  --compressed -v

NOTE that this is a POST with “multipart/form-data” which would normally qualify as a simple request, but because there’s an authorization header it is no longer “simple” and a pre-flight request is sent by the browser. The Sec-* headers are not on the CORS safelist either.

(NOTE curl uses -X POST by default with “multipart/form-data”.)

Interesting side note for browser JavaScript coders - There is a list of “Forbidden Request Headers”. These are request headers that are not allowed to be added or changed by code, because the browser has complete control over these particular headers. See: https://fetch.spec.whatwg.org/#forbidden-header-name

Note that both Infura/eth and Infura/IPFS API endpoints return an access-control-allow-origin header with the exact same URL as the Origin header provided in the request, i.e. not a ‘*’ or wildcard. This is because of the CORS rule that says authentication is not allowed, where the allowed origin is a wildcard, and Infura often (esp. IPFS) requires auth.

i.e. if the Origin header is “http://localhost:3000” then the same origin will be returned in the access-control-allow-origin header. In fact if the value for the Origin header is “whatever” then “whatever” will be returned.

Example of a CORS error

NOTE: There is one source of CORS error from Infura. Below is a pre-flight for a ethereum endpoint request that includes an authorization header (i.e. the API Secret Key was included):

curl 'https://mainnet.infura.io/v3/c1f6fa004ded489fa95d3d84219e8860' \
  -X 'OPTIONS' \
  -H 'authority: mainnet.infura.io' \
  -H 'accept: */*' \
  -H 'accept-language: en-AU,en-GB-oxendict;q=0.9,en;q=0.8' \
  -H 'access-control-request-headers: **authorization**,content-type' \
  -H 'access-control-request-method: POST' \
  -H 'cache-control: max-age=0' \
  -H 'origin: http://localhost:3001' \
  -H 'referer: http://localhost:3001/' \
  -H 'sec-fetch-dest: empty' \
  -H 'sec-fetch-mode: cors' \
  -H 'sec-fetch-site: cross-site' \
  -H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36' \
  --compressed -v

Note that a preflight request does not actually include the authorization header, but it does signal that the actual request will include it (see underlined above).

The response is:

< HTTP/2 200 
< date: Mon, 14 Nov 2022 23:02:37 GMT
< content-length: 0
< vary: Accept-Encoding
< vary: Origin
< vary: Access-Control-Request-Method
< vary: Access-Control-Request-Headers

As you can see, access-control-allow-origin, etc, are missing in the response, the browser will show a CORS error in the console.

And the response when the authorization header is not included is:

< HTTP/2 200 
< date: Mon, 14 Nov 2022 23:12:57 GMT
< content-length: 0
< access-control-allow-headers: Content-Type
< access-control-allow-methods: POST
< access-control-allow-origin: http://localhost:3001
< access-control-max-age: 86400
< vary: Accept-Encoding
< vary: Origin
< vary: Access-Control-Request-Method
< vary: Access-Control-Request-Headers

i.e. access-control-allow-origin, etc, are present. So, if the API secret is included in the request (only from a browser, not from backend code), then the request will fail with a CORS error.

This is by design because the API Secret Key should never be included in browser JavaScript frontend code because it’s easily visible there.

Dealing with CORS errors

The bottom line is: if you get a CORS error, then there’s a good reason (such as we want to disallow including API secret key in the front end) or changes need to be made on the Infura side to correct them.

Alternatively, you can move your Infura access to the backend - if your architecture allows this option. The backend does not send CORS related headers (unless you code them in) and Infura will not send back CORS related responses, and even if it did it’s only the browser that makes decisions based on them, so CORS does not come into the picture.

If your architecture does not lend itself to moving your Infura access to the backend, you can use a “CORS proxy”. This takes the requests from your browser based JavaScript, passes them on to Infura, and adds the headers to the response so that the browser is satisfied. This solution is like a mini-backend. It’s a backend that handles CORS concerns only. We plan to document an example solution of this type soon.

Summary

CORS is enforced in the browser, this is why your backend code or a curl request “works”. Both these environments are not doing any CORS checking, and are not sending CORS headers.

If you do get a CORS error, open a support case and provide the code generating the request, or the equivalent curl request, so that we can understand what is happening.

2 Likes