Fetch: Cross-Origin Requests
Browsers restrict cross-origin HTTP requests for security. CORS (Cross-Origin Resource Sharing) is the mechanism that safely relaxes these restrictions.
Same-Origin Policy
Two URLs have the same origin if they share the same protocol, host, and port:
https://example.com/page1 β
https://example.com/page2 β Same origin
β
https://api.example.com β Different host
http://example.com β Different protocol
https://example.com:8080 β Different port
Simple Requests
A request is βsimpleβ if:
- Method is
GET,HEAD, orPOST - Only simple headers (
Accept,Accept-Language,Content-Language,Content-Type) Content-Typeisapplication/x-www-form-urlencoded,multipart/form-data, ortext/plain
Simple requests are sent directly β the browser adds the Origin header and checks the responseβs Access-Control-Allow-Origin.
Preflighted Requests
For non-simple requests (e.g., JSON content type, custom headers, PUT/DELETE), the browser sends a preflight OPTIONS request first:
Browser Server
β β
βββ OPTIONS /api/data β
β Origin: https://myapp.com β
β Access-Control-Request-Method: POST β
β Access-Control-Request-Headers: authorization β
β β
ββββ 204 No Content β
β Access-Control-Allow-Origin: https://myapp.com β
β Access-Control-Allow-Methods: POST, GET β
β Access-Control-Allow-Headers: authorization β
β Access-Control-Max-Age: 86400 β
β β
βββ POST /api/data (real request) β
β β
Server Response Headers
The server must include these CORS headers:
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400
Fetch and CORS
// Simple cross-origin GET
const response = await fetch('https://api.example.com/data');
// Cross-origin with custom headers (triggers preflight)
const response = await fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Custom-Header': 'value' // triggers preflight
},
body: JSON.stringify({ key: 'value' })
});
Credentials (Cookies)
To send cookies cross-origin:
// Both sides needed:
// Client:
const response = await fetch('https://api.example.com/data', {
credentials: 'include'
});
// Server must also allow:
// Access-Control-Allow-Credentials: true
// Access-Control-Allow-Origin: https://myapp.com (NOT '*')
When credentials is 'include', the server cannot use Access-Control-Allow-Origin: * β it must be explicit.
CORS Modes
fetch(url, { mode: 'cors' }); // default β must have CORS headers
fetch(url, { mode: 'no-cors' }); // opaque response (can't read)
fetch(url, { mode: 'same-origin' }); // reject cross-origin
Handling CORS Errors
A CORS error canβt be caught as a normal HTTP error β the fetch promise rejects:
try {
const response = await fetch('https://api.other-site.com/data');
// If CORS fails, this line never runs
const data = await response.json();
} catch (err) {
// This catches the CORS error
console.error('CORS error or network failure:', err.message);
}
CORS Headers Table
| Header | Purpose |
|---|---|
Access-Control-Allow-Origin | Which origins are allowed (* or specific) |
Access-Control-Allow-Methods | Allowed HTTP methods |
Access-Control-Allow-Headers | Allowed custom headers |
Access-Control-Allow-Credentials | Whether to allow cookies |
Access-Control-Max-Age | How long to cache preflight |
Access-Control-Expose-Headers | Which headers the client can read |
CORS for Images and Media
Loading images, scripts, or CSS cross-origin usually works without CORS. But reading pixel data from a <canvas> with a cross-origin image requires CORS:
const img = new Image();
img.crossOrigin = 'anonymous';
img.src = 'https://other-site.com/photo.jpg';
img.onload = function() {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const data = ctx.getImageData(0, 0, 100, 100); // needs CORS
};
Key Takeaways
- Same-origin requests work freely; cross-origin needs CORS headers
- Simple requests (GET, simple headers) donβt need preflight
- Non-simple requests trigger an OPTIONS preflight
- Set
credentials: 'include'to send cookies cross-origin - The server must respond with the right CORS headers
Access-Control-Allow-Origin: *doesnβt work with credentials- CORS errors reject the fetch promise β handle in
catch - Use
mode: 'no-cors'for requests where you donβt need to read the response