Fetch: Cross-Origin Requests Β· Astro Tech Blog

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, or POST
  • Only simple headers (Accept, Accept-Language, Content-Language, Content-Type)
  • Content-Type is application/x-www-form-urlencoded, multipart/form-data, or text/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

HeaderPurpose
Access-Control-Allow-OriginWhich origins are allowed (* or specific)
Access-Control-Allow-MethodsAllowed HTTP methods
Access-Control-Allow-HeadersAllowed custom headers
Access-Control-Allow-CredentialsWhether to allow cookies
Access-Control-Max-AgeHow long to cache preflight
Access-Control-Expose-HeadersWhich 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