Браузер застосовує політику одного джерела (same-origin policy): JavaScript зі сторінки https://app.example.com не може читати відповіді від https://api.other.com. Джерело (origin) - це схема + домен + порт: http і https, example.com і api.example.com, порти 3000 і 8000 - різні джерела.
Навіщо: інакше будь-який сайт, який ви відкрили, міг би від вашого імені (з вашими cookies) читати пошту, банківський кабінет чи адмінку.
CORS (Cross-Origin Resource Sharing) - спосіб, яким сервер дозволяє певним джерелам читати свої відповіді, через заголовки:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Попередній запит (preflight). Для «непростих» запитів браузер спершу надсилає OPTIONS, щоб запитати дозволу:
- методи, крім
GET,HEAD,POST; - власні заголовки (
Authorization,X-Requested-With); Content-Type: application/json(простими вважаються лише формові типи йtext/plain).
OPTIONS /api/orders
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: content-type, authorization
Сервер відповідає, що дозволено (Access-Control-Allow-Methods, -Headers, -Max-Age для кешування дозволу). Лише після цього йде справжній запит.
Що важливо розуміти:
- CORS виконує браузер, а не сервер.
curl, Postman і бекенд-код не мають жодних обмежень. CORS - не захист API від чужих клієнтів; - простий запит доходить до сервера і виконується - браузер лише не дає JavaScript прочитати відповідь. Тому CORS не замінює захист від CSRF;
- з cookies (
credentials: 'include') забороненоAccess-Control-Allow-Origin: *- потрібне конкретне джерело іAccess-Control-Allow-Credentials: true; - помилка CORS у консолі часто маскує іншу: сервер повернув 500 чи 404 без CORS-заголовків, і браузер повідомляє про CORS, а не про справжню причину. Варто дивитися запит у вкладці Network.
У Laravel CORS обробляє вбудований middleware HandleCors з налаштуваннями в config/cors.php (публікується php artisan config:publish cors):
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_origins' => ['https://app.example.com'],
'supports_credentials' => true,
Як обійтися без CORS: розмістити фронтенд і API на одному джерелі (Laravel віддає і сторінки, і /api), або проксувати запити через сервер розробки Vite (server.proxy). Тоді запити same-origin, і CORS не потрібен.