Фронтенд на https://app.myshop.ru делает запрос к внешнему API https://api.payments.ru. Браузер блокирует запрос с ошибкой: "has been blocked by CORS policy". Curl и Postman работают нормально. В чём дело?
Origin — комбинация протокола, домена и порта. Браузер блокирует запросы к чужому origin по умолчанию:
https://app.myshop.ru:443 → текущий origin
https://app.myshop.ru/api — тот же origin ✓ (путь не имеет значения)
https://api.myshop.ru — другой origin ✗ (другой поддомен)
http://app.myshop.ru — другой origin ✗ (другой протокол)
https://app.myshop.ru:8080 — другой origin ✗ (другой порт)
https://payments.ru — другой origin ✗ (другой домен)CORS (Cross-Origin Resource Sharing) — механизм, при котором сервер явно разрешает запросы через HTTP-заголовки ответа:
Access-Control-Allow-Origin: https://app.myshop.ru
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, AuthorizationИли открыт для всех (публичные API):
Access-Control-Allow-Origin: *Для нестандартных методов или заголовков браузер сначала спрашивает разрешения через OPTIONS:
OPTIONS /api/payments HTTP/1.1
Origin: https://app.myshop.ru
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Authorization
← ответ сервера:
Access-Control-Allow-Origin: https://app.myshop.ru
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Authorization
Access-Control-Max-Age: 86400Preflight нужен если: метод не GET/POST, или есть нестандартные заголовки (Authorization, X-Custom-Header).
// Клиент — явно разрешить отправку куков
fetch('https://api.payments.ru/profile', {
credentials: 'include',
})
// Сервер — при credentials нельзя использовать * для origin!
// Access-Control-Allow-Origin: https://app.myshop.ru (конкретный домен)
// Access-Control-Allow-Credentials: trueОшибка 1: пытаются решить CORS на клиенте
// Сломано: клиент не может обойти CORS — это защита браузера
fetch('https://api.partner.ru/data', {
headers: {
'Access-Control-Allow-Origin': '*', // НЕ РАБОТАЕТ — это заголовок ответа
},
})
// Исправлено: CORS настраивается только на сервере:
// res.setHeader('Access-Control-Allow-Origin', 'https://app.myshop.ru')Ошибка 2: credentials с wildcard origin
// Сломано: нельзя одновременно credentials и Allow-Origin: *
// Сервер: Access-Control-Allow-Origin: * ← проблема
// Клиент: fetch(url, { credentials: 'include' })
// Исправлено: сервер указывает конкретный origin:
// Access-Control-Allow-Origin: https://app.myshop.ru
// Access-Control-Allow-Credentials: trueОшибка 3: не различают CORS-ошибку от сетевой
// Сломано: непонятное сообщение
try {
await fetch('https://api.partner.ru/data')
} catch (err) {
console.error('Ошибка:', err.message) // 'Failed to fetch' — почему?
}
// Исправлено: проверяем тип ошибки
try {
const res = await fetch('https://api.partner.ru/data')
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return await res.json()
} catch (err) {
if (err instanceof TypeError && err.message.includes('Failed to fetch')) {
throw new Error('CORS: сервер не разрешил запрос с вашего домена')
}
throw err
}Вместо настройки CORS на внешнем API — проксируйте через свой сервер:
// Next.js API route — фронтенд обращается к /api/payments (тот же origin)
export async function POST(request) {
const body = await request.json()
const response = await fetch('https://api.payments.ru/charge', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + process.env.PAYMENTS_API_KEY,
},
body: JSON.stringify(body),
})
return Response.json(await response.json())
}Access-Control-Allow-Origin: *Анализ origins и обёртка для fetch с корректной обработкой CORS-ошибок
// Утилита для определения: нужен ли CORS для данного URL
function needsCors(currentOrigin, targetUrl) {
try {
const targetOrigin = new URL(targetUrl).origin
return targetOrigin !== currentOrigin
} catch {
return false
}
}
// Демонстрация разбора origins
const myOrigin = 'https://app.myshop.ru'
const endpoints = [
'https://app.myshop.ru/api/products',
'https://app.myshop.ru/api/orders',
'https://api.myshop.ru/products',
'http://app.myshop.ru/products',
'https://app.myshop.ru:8080/products',
'https://payments-gateway.ru/charge',
]
console.log(`Текущий origin: ${myOrigin}\n`)
console.log('URL → CORS нужен?')
endpoints.forEach(url => {
const cors = needsCors(myOrigin, url)
const icon = cors ? '✗ CORS' : '✓ same'
console.log(`${icon} ${url}`)
})
// ✓ same https://app.myshop.ru/api/products
// ✓ same https://app.myshop.ru/api/orders
// ✗ CORS https://api.myshop.ru/products
// ✗ CORS http://app.myshop.ru/products
// ✗ CORS https://app.myshop.ru:8080/products
// ✗ CORS https://payments-gateway.ru/charge
// Обёртка fetch с правильной обработкой ошибок
async function fetchApi(url, options = {}) {
try {
const response = await fetch(url, {
headers: { 'Content-Type': 'application/json' },
...options,
})
if (!response.ok) {
const err = await response.json().catch(() => ({ message: response.statusText }))
throw new Error(err.message || `HTTP ${response.status}`)
}
return { data: await response.json(), error: null }
} catch (err) {
if (err instanceof TypeError && err.message.includes('Failed to fetch')) {
return {
data: null,
error: 'CORS: сервер не добавил Access-Control-Allow-Origin. Настройте CORS на сервере или используйте прокси.',
}
}
return { data: null, error: err.message }
}
}
// Симуляция CORS-ошибки
async function demo() {
console.log('\n=== Симуляция ответов ===')
// Симуляция успешного CORS-ответа
const mockHeaders = {
'Access-Control-Allow-Origin': 'https://app.myshop.ru',
'Access-Control-Allow-Methods': 'GET, POST',
'Content-Type': 'application/json',
}
console.log('\nОтвет с корректными CORS-заголовками:')
Object.entries(mockHeaders).forEach(([k, v]) => console.log(` ${k}: ${v}`))
console.log(' → браузер пропускает ответ')
// Симуляция CORS-ошибки
const corsError = new TypeError('Failed to fetch')
const handled = corsError instanceof TypeError && corsError.message.includes('Failed to fetch')
? { data: null, error: 'CORS: сервер не добавил Access-Control-Allow-Origin' }
: { data: null, error: corsError.message }
console.log('\nОтвет без CORS-заголовков:')
console.log(' TypeError:', corsError.message)
console.log(' Обработанная ошибка:', handled.error)
}
demo()Фронтенд на https://app.myshop.ru делает запрос к внешнему API https://api.payments.ru. Браузер блокирует запрос с ошибкой: "has been blocked by CORS policy". Curl и Postman работают нормально. В чём дело?
Origin — комбинация протокола, домена и порта. Браузер блокирует запросы к чужому origin по умолчанию:
https://app.myshop.ru:443 → текущий origin
https://app.myshop.ru/api — тот же origin ✓ (путь не имеет значения)
https://api.myshop.ru — другой origin ✗ (другой поддомен)
http://app.myshop.ru — другой origin ✗ (другой протокол)
https://app.myshop.ru:8080 — другой origin ✗ (другой порт)
https://payments.ru — другой origin ✗ (другой домен)CORS (Cross-Origin Resource Sharing) — механизм, при котором сервер явно разрешает запросы через HTTP-заголовки ответа:
Access-Control-Allow-Origin: https://app.myshop.ru
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, AuthorizationИли открыт для всех (публичные API):
Access-Control-Allow-Origin: *Для нестандартных методов или заголовков браузер сначала спрашивает разрешения через OPTIONS:
OPTIONS /api/payments HTTP/1.1
Origin: https://app.myshop.ru
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Authorization
← ответ сервера:
Access-Control-Allow-Origin: https://app.myshop.ru
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Authorization
Access-Control-Max-Age: 86400Preflight нужен если: метод не GET/POST, или есть нестандартные заголовки (Authorization, X-Custom-Header).
// Клиент — явно разрешить отправку куков
fetch('https://api.payments.ru/profile', {
credentials: 'include',
})
// Сервер — при credentials нельзя использовать * для origin!
// Access-Control-Allow-Origin: https://app.myshop.ru (конкретный домен)
// Access-Control-Allow-Credentials: trueОшибка 1: пытаются решить CORS на клиенте
// Сломано: клиент не может обойти CORS — это защита браузера
fetch('https://api.partner.ru/data', {
headers: {
'Access-Control-Allow-Origin': '*', // НЕ РАБОТАЕТ — это заголовок ответа
},
})
// Исправлено: CORS настраивается только на сервере:
// res.setHeader('Access-Control-Allow-Origin', 'https://app.myshop.ru')Ошибка 2: credentials с wildcard origin
// Сломано: нельзя одновременно credentials и Allow-Origin: *
// Сервер: Access-Control-Allow-Origin: * ← проблема
// Клиент: fetch(url, { credentials: 'include' })
// Исправлено: сервер указывает конкретный origin:
// Access-Control-Allow-Origin: https://app.myshop.ru
// Access-Control-Allow-Credentials: trueОшибка 3: не различают CORS-ошибку от сетевой
// Сломано: непонятное сообщение
try {
await fetch('https://api.partner.ru/data')
} catch (err) {
console.error('Ошибка:', err.message) // 'Failed to fetch' — почему?
}
// Исправлено: проверяем тип ошибки
try {
const res = await fetch('https://api.partner.ru/data')
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return await res.json()
} catch (err) {
if (err instanceof TypeError && err.message.includes('Failed to fetch')) {
throw new Error('CORS: сервер не разрешил запрос с вашего домена')
}
throw err
}Вместо настройки CORS на внешнем API — проксируйте через свой сервер:
// Next.js API route — фронтенд обращается к /api/payments (тот же origin)
export async function POST(request) {
const body = await request.json()
const response = await fetch('https://api.payments.ru/charge', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + process.env.PAYMENTS_API_KEY,
},
body: JSON.stringify(body),
})
return Response.json(await response.json())
}Access-Control-Allow-Origin: *Анализ origins и обёртка для fetch с корректной обработкой CORS-ошибок
// Утилита для определения: нужен ли CORS для данного URL
function needsCors(currentOrigin, targetUrl) {
try {
const targetOrigin = new URL(targetUrl).origin
return targetOrigin !== currentOrigin
} catch {
return false
}
}
// Демонстрация разбора origins
const myOrigin = 'https://app.myshop.ru'
const endpoints = [
'https://app.myshop.ru/api/products',
'https://app.myshop.ru/api/orders',
'https://api.myshop.ru/products',
'http://app.myshop.ru/products',
'https://app.myshop.ru:8080/products',
'https://payments-gateway.ru/charge',
]
console.log(`Текущий origin: ${myOrigin}\n`)
console.log('URL → CORS нужен?')
endpoints.forEach(url => {
const cors = needsCors(myOrigin, url)
const icon = cors ? '✗ CORS' : '✓ same'
console.log(`${icon} ${url}`)
})
// ✓ same https://app.myshop.ru/api/products
// ✓ same https://app.myshop.ru/api/orders
// ✗ CORS https://api.myshop.ru/products
// ✗ CORS http://app.myshop.ru/products
// ✗ CORS https://app.myshop.ru:8080/products
// ✗ CORS https://payments-gateway.ru/charge
// Обёртка fetch с правильной обработкой ошибок
async function fetchApi(url, options = {}) {
try {
const response = await fetch(url, {
headers: { 'Content-Type': 'application/json' },
...options,
})
if (!response.ok) {
const err = await response.json().catch(() => ({ message: response.statusText }))
throw new Error(err.message || `HTTP ${response.status}`)
}
return { data: await response.json(), error: null }
} catch (err) {
if (err instanceof TypeError && err.message.includes('Failed to fetch')) {
return {
data: null,
error: 'CORS: сервер не добавил Access-Control-Allow-Origin. Настройте CORS на сервере или используйте прокси.',
}
}
return { data: null, error: err.message }
}
}
// Симуляция CORS-ошибки
async function demo() {
console.log('\n=== Симуляция ответов ===')
// Симуляция успешного CORS-ответа
const mockHeaders = {
'Access-Control-Allow-Origin': 'https://app.myshop.ru',
'Access-Control-Allow-Methods': 'GET, POST',
'Content-Type': 'application/json',
}
console.log('\nОтвет с корректными CORS-заголовками:')
Object.entries(mockHeaders).forEach(([k, v]) => console.log(` ${k}: ${v}`))
console.log(' → браузер пропускает ответ')
// Симуляция CORS-ошибки
const corsError = new TypeError('Failed to fetch')
const handled = corsError instanceof TypeError && corsError.message.includes('Failed to fetch')
? { data: null, error: 'CORS: сервер не добавил Access-Control-Allow-Origin' }
: { data: null, error: corsError.message }
console.log('\nОтвет без CORS-заголовков:')
console.log(' TypeError:', corsError.message)
console.log(' Обработанная ошибка:', handled.error)
}
demo()Напиши функцию fetchWithCors(url, options), которая делает fetch-запрос, обрабатывает HTTP-ошибки и CORS-ошибки (TypeError "Failed to fetch"), возвращая объект { data, error }.
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`); return { data: null, error: err.message }