CORS + Fetch API | Summary

TL;DR

  1. Browsers always add Origin header to CORS requests. They act as trusted mediator; validate the request against preflight response header(s), and such.
  2. If a fetch request body is JSON, declared per Content-Type: application/json, then it is an "Unsafe" CORS request. (That declaration should be unnecessary, as the service endpoint should be expecting content of that one type.)
  3. Cookie header is normally not sent with requests of Fetch API. To send them, request options must include credentials:
    • fetch(url, {credentials: "include"})
    • In doing so,Access-Control-Allow-Origin response header value must be set to a specific origin, not the wildcard, "*"; browsers interpret it here as a literal.
  4. Though headers beyond those of "Safe" list must be explicitly declared in preflight (OPTIONS) request (Access-Control-Allow-Headers:), the Cookie header needn't be declared if the Authorization header is declared and fetch option 'credentials: "include"' is set.

Be advised that browsers will falsely report CORS errors in certain cases. For example, if the CORS request is to endpoints not handled by the router, if the service is unavailable (500 Internal Server Error), and other cases. Each browser vendor has their own set of quirks. Chrome tends to provide the most insight (versus Firefox) into CORS issues.

CORS @ safe requests

CORS requests that are automatically allowed by browsers; sans preflight (OPTIONS) request.

Example CORS request:

Request Headers

GET /request
Host: anywhere.com
Origin: https://javascript.info
...
# cURL-equivalent request
curl -X GET \
  -H 'Origin: https://javascript.info' \
  -H 'Host: anywhere.com' \
  https://anywhere.com/request

Response Headers (Permissive):

200 OK
Content-Type: text/html; charset=UTF-8
Access-Control-Allow-Origin: https://javascript.info

Safe response headers

Browser allows javascript only these, by default:

Cache-Control
Content-Language
Content-Type
Expires
Last-Modified
Pragma

To grant JavaScript access to any other response header, the server must send Access-Control-Expose-Headers, e.g.,

200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 12345
API-Key: 2c9de507f2c54aa1
Access-Control-Allow-Origin: https://javascript.info
Access-Control-Expose-Headers: Content-Length,API-Key

CORS @ unsafe requests

let response = await fetch('https://site.com/service.json', {
  method: 'PATCH',
  headers: {
    'Content-Type': 'application/json',
    'API-Key': 'secret'
  }
})

Browser sends Preflight, OPTIONS method, request

Preflight

Request

OPTIONS /service.json
Host: site.com
Origin: https://javascript.info
Access-Control-Request-Method: PATCH
Access-Control-Request-Headers: Content-Type,API-Key

If the server agrees to serve the requests, then it should respond with empty body, status 200 and headers:

Response

Access-Control-Allow-Origin: https://javascript.info
Access-Control-Allow-Methods: PATCH
Access-Control-Allow-Headers: Content-Type,API-Key.

Server may have list of such to service all expected requests

200 OK
Access-Control-Allow-Origin: https://javascript.info
Access-Control-Allow-Methods: PUT,PATCH,DELETE
Access-Control-Allow-Headers: API-Key,Content-Type,If-Modified-Since,Cache-Control
Access-Control-Max-Age: 86400

Main

Request

PATCH /service.json
Host: site.com
Content-Type: application/json
API-Key: secret
Origin: https://javascript.info

Response

Access-Control-Allow-Origin: https://javascript.info

Credentials

A cross-origin request initiated by JavaScript code by default does not bring any credentials (headers: Cookie or WWW-Authenticate).

For example, fetch('http://another.com') does not send any cookies, even those that belong to another.com domain.

To send credentials using Fetcch API, add the option credentials: "include", ...

fetch('http://another.com', {
  credentials: "include"
})

Preflight

Request headers

OPTIONS /service.json
Host: site.com
Origin: https://javascript.info
Access-Control-Request-Method: PATCH
Access-Control-Request-Headers: Authorization

Response headers (Permissive)

200 OK
Access-Control-Allow-Origin: https://javascript.info
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: Authorization

Main

Request

OPTIONS /service.json
Host: site.com
Origin: https://javascript.info
Access-Control-Request-Method: PATCH
Access-Control-Request-Headers: Authorization

Response

Access-Control-Allow-Origin: https://javascript.info
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: *