← JavaScript/CORS: запросы на другой сайт#131 из 383← ПредыдущийСледующий →+20 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

CORS: запросы на другой сайт

Фронтенд на https://app.myshop.ru делает запрос к внешнему API https://api.payments.ru. Браузер блокирует запрос с ошибкой: "has been blocked by CORS policy". Curl и Postman работают нормально. В чём дело?

Что такое Same-Origin Policy

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

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: *

На основе предыдущих уроков

  • fetch: запросы, которые CORS контролирует
  • async/await: асинхронная обработка ответов
  • try/catch: обработка CORS-ошибок
  • Preflight-запросы

    Для нестандартных методов или заголовков браузер сначала спрашивает разрешения через 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: 86400

    Preflight нужен если: метод не GET/POST, или есть нестандартные заголовки (Authorization, X-Custom-Header).

    Куки и credentials

    // Клиент — явно разрешить отправку куков
    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
    }

    Proxy как решение CORS

    Вместо настройки 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())
    }

    В реальных проектах

  • Локальная разработка: Vite/webpack proxy убирает CORS при разработке
  • Продакшн: API Gateway (AWS, Nginx) добавляет CORS-заголовки централизованно
  • Публичные API (OpenWeather, GitHub): ставят Access-Control-Allow-Origin: *
  • Безопасность: CORS защищает пользователя от злоумышленных сайтов, но не защищает API от прямых запросов через curl
  • Примеры

    Анализ 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()

    CORS: запросы на другой сайт

    Фронтенд на https://app.myshop.ru делает запрос к внешнему API https://api.payments.ru. Браузер блокирует запрос с ошибкой: "has been blocked by CORS policy". Curl и Postman работают нормально. В чём дело?

    Что такое Same-Origin Policy

    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

    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: *

    На основе предыдущих уроков

  • fetch: запросы, которые CORS контролирует
  • async/await: асинхронная обработка ответов
  • try/catch: обработка CORS-ошибок
  • Preflight-запросы

    Для нестандартных методов или заголовков браузер сначала спрашивает разрешения через 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: 86400

    Preflight нужен если: метод не GET/POST, или есть нестандартные заголовки (Authorization, X-Custom-Header).

    Куки и credentials

    // Клиент — явно разрешить отправку куков
    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
    }

    Proxy как решение CORS

    Вместо настройки 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())
    }

    В реальных проектах

  • Локальная разработка: Vite/webpack proxy убирает CORS при разработке
  • Продакшн: API Gateway (AWS, Nginx) добавляет CORS-заголовки централизованно
  • Публичные API (OpenWeather, GitHub): ставят Access-Control-Allow-Origin: *
  • Безопасность: CORS защищает пользователя от злоумышленных сайтов, но не защищает API от прямых запросов через curl
  • Примеры

    Анализ 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 }

    Загружаем среду выполнения...
    Загружаем AI-помощника...