Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 14 из 25 ~18 мин +50 XP

CORS: чиним браузерные запросы

Фронтенд на localhost:3000 стучится в API на :8080, сервер отрабатывает со статусом 200, а в консоли браузера краснеет blocked by CORS policy — и именно здесь chi cors превращает загадку в настройку из одного middleware. Разберём preflight, заголовки Access-Control-Allow-Origin, поля cors.Options и отладку ошибок, которых не видно на сервере.

Содержание урока · 11 разделов

Сцена, которую видел каждый: фронтенд-разработчик запускает приложение с localhost:3000, API на том же ноутбуке слушает :8080, и каждый fetch падает с ошибкой про CORS — при том что в логах сервера все запросы зелёные и со статусом 200. Сервер невиновен, браузер не сломан: это Same-Origin Policy, правило браузера о том, что страница с одного происхождения не может без разрешения читать ответы другого. CORS — Cross-Origin Resource Sharing — и есть формат этого разрешения: сервер объявляет заголовками, кому и что можно. В chi разрешение включается одним middleware из экосистемы роутера. В этом уроке — кто блокирует запросы на самом деле, что такое preflight и почему он не доходит до ваших хендлеров, из чего состоит cors.Options и как отлаживать ошибку, причиной которой обязан сервер, а симптомом — страдает только браузер.

CORS: блокирует не сервер, а браузер#

Same-Origin Policy — защита пользователя: вкладка с сайта банка не должна уметь тихо читать ответы вашего API, потому что вместе с ними уйдут cookies. Совпадение происхождения означает схему, домен и порт — localhost:3000 и localhost:8080 считаются разными; сами термины разобраны в глоссарии. CORS — механизм, которым сервер добровольно ослабляет это правило: в ответе появляется заголовок Access-Control-Allow-Origin, и браузер решает, доверять ли ему. Отсюда главный факт урока: CORS исполняет исключительно браузер. curl, Postman и любой серверный клиент про заголовки согласия ничего не спрашивают и получают ответ как обычно. Если Postman показывает данные, а fetch в браузере — ошибку, значит сервер просто не отдал разрешения, и чинить надо его конфигурацию, а не фронтенд. Модуль go-chi/cors — самый ходовой вариант в cors go-экосистеме, и подключается он как обычный middleware.

cors.Handler в цепочке

GO
package main

import (
        "net/http"

        "github.com/go-chi/chi/v5"
        "github.com/go-chi/cors"
)

func main() {
        r := chi.NewRouter()

        r.Use(cors.Handler(cors.Options{
                AllowedOrigins:   []string{"http://localhost:3000"},
                AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE"},
                AllowedHeaders:   []string{"Content-Type", "Authorization"},
                AllowCredentials: true,
                MaxAge:           300,
        }))

        r.Get("/api/notes", listNotes)
        r.Post("/api/notes", createNote)

        http.ListenAndServe(":8080", r)
}

cors.Handler живёт в отдельном модуле github.com/go-chi/cors — добавьте его go get-ом рядом с chi. Регистрация через r.Use до маршрутов, как у любого middleware: слой ставит разрешающие заголовки на каждый ответ и сам отвечает на preflight. Обратите внимание на AllowedOrigins: точный адрес фронтенда, а не звёздочка — вместе с AllowCredentials: true звёздочка не работает, почему — ниже. Для запуска нужен локальный Go: песочница сайта HTTP-сервер не поднимает.

Preflight: невидимый OPTIONS-запрос#

Перед многими запросами браузер делает предварительный: отправляет OPTIONS с заголовком Origin — откуда пришла страница — и Access-Control-Request-Method — какой метод собирается использовать; если запрос несёт кастомные заголовки вроде Authorization, к ним добавляется Access-Control-Request-Headers. Это preflight, предзапрос: браузер сначала спрашивает разрешения, и лишь получив заголовки согласия в ответе, отправляет настоящий запрос. cors.Handler перехватывает preflight сам и отвечает, не доходя до ваших хендлеров — в логах это выглядит как OPTIONS, которого вы не регистрировали. Если источник не в списке разрешённых, preflight уходит без заголовков согласия — в go-chi/cors с кодом 403 — и настоящий запрос не отправляется вовсе: браузер отказывается сам, не тратя трафик на заведомо запрещённый вызов. Посмотреть preflight глазами curl можно в следующем блоке.

Preflight глазами curl

GO
# preflight: браузер спрашивает разрешение до отправки POST
$ curl -i -X OPTIONS http://localhost:8080/api/notes \
    -H "Origin: http://localhost:3000" \
    -H "Access-Control-Request-Method: POST" \
    -H "Access-Control-Request-Headers: content-type"

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 300

Запрос OPTIONS на маршрут, которого в коде нет, — это preflight. cors.Handler видит Origin и Access-Control-Request-Method, сверяет их с cors.Options и отвечает сам: разрешение с нужными заголовками, до хендлера дело не доходит. Смените Origin на чужой домен — заголовки согласия исчезнут, и браузер такой preflight забракует. Тем же способом проверяют настройки, когда fetch падает: ответ curl показывает, чего именно не хватает.

Проверка: анатомия preflight

Что браузер отправляет в preflight перед POST с application/json?

Простые и непростые запросы#

Preflight нужен не всем. Простыми браузер считает запросы, которые исторически умели отправлять HTML-формы: GET, HEAD и POST с телом в text/plain, application/x-www-form-urlencoded или multipart/form-data и без кастомных заголовков — их браузер шлёт сразу, а проверяет уже заголовки ответа. Всё остальное требует preflight. И вот тут ошибка номер один при отладке JSON API: POST с Content-Type: application/json — непростой, потому что такой content-type формы не умеют. Разработчик смотрит на обычный POST и не понимает, откуда в логе OPTIONS. Любая авторизация через заголовок, например Authorization, тоже делает запрос непростым — как и большая часть настоящих JSON API. Проверка простая: если fetch падает, а в логе сервера нет входящего запроса — отказал preflight, и до хендлера дело не дошло.

cors.Options: что настраивать#

cors.Handler принимает структуру cors.Options, и почти вся настройка сводится к четырём полям: AllowedOrigins — какие происхождения пускать, AllowedMethods — какие HTTP-методы, AllowedHeaders — какие заголовки запроса разрешить сверх простых, AllowCredentials — можно ли слать cookies и авторизацию. Поле ExposedHeaders пропускают часто, а зря: без Access-Control-Expose-Headers браузер не даст JavaScript прочитать кастомные заголовки ответа — например Request-ID из урока про встроенные middleware. MaxAge говорит браузеру, сколько секунд кэшировать ответ на preflight: чем больше, тем реже летят лишние OPTIONS. Разрешать стоит узко: конкретный домен фронтенда, а не звёздочку, и только реально используемые методы и заголовки — каждый пункт списка это доступ, который вы выдаёте чужим страницам.

Поля cors.Options#

  • AllowedOrigins — список допустимых происхождений или звёздочка
  • AllowedMethods — HTTP-методы, которые разрешено отправлять
  • AllowedHeaders — кастомные заголовки запроса: Authorization, Content-Type, X-Request-ID
  • ExposedHeaders — заголовки ответа, которые JavaScript сможет прочитать
  • AllowCredentials — разрешение на cookies и авторизацию в запросе
  • MaxAge — на сколько секунд браузер кэширует ответ на preflight

Проверка: что видит JavaScript

Сервер обработал POST и вернул 200, но без Access-Control-Allow-Origin. Что увидит JavaScript в браузере?

Что решит cors.Handler?

Прочитайте код и выберите, что он выведет:

package main

import "fmt"

func main() {
        allowed := map[string]bool{
                "http://localhost:3000": true,
        }

        requests := []string{
                "http://localhost:3000",
                "https://evil.example.net",
        }

        for _, origin := range requests {
                if allowed[origin] {
                        fmt.Println(origin, "-> заголовки согласия")
                } else {
                        fmt.Println(origin, "-> 403, заголовков нет")
                }
        }
}

Соберите подключение CORS

Впишите имя функции-обёртки и два поля cors.Options: список доменов и разрешение на заголовки авторизации.

import "github.com/go-chi/cors" r.Use(cors.(cors.Options{ : []string{"https://app.example.com"}, AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"}, : []string{"Authorization", "Content-Type"}, }))

Отладка CORS: смотреть надо на сервер#

Ошибка CORS в консоли браузера похожа на приговор без деталей: браузер сознательно скрывает подробности, чтобы не отдавать чужому сайту информацию о вашем ответе. Поэтому отлаживают со стороны сервера. Первым делом повторите preflight curl-ом: OPTIONS с заголовками Origin и Access-Control-Request-Method — и смотрите, какие Access-Control-* заголовки пришли в ответ. Пришли и правильные — дело в самом запросе: проверяйте, совпадает ли Origin посимвольно, включая порт и https. Не пришли — перечитайте AllowedOrigins, AllowedMethods и AllowedHeaders: чаще всего не хватает одного пункта из Access-Control-Request-Headers. Полезно логировать Origin в middleware: в многодоменных проектах разрешение регулярно забывают добавить для нового домена, и лог это покажет сразу.

Сценарии запросов: что делает браузер#

СценарийБраузерЧто нужно серверу
GET без кастомных заголовковотправляет сразу, проверяет заголовки ответаAccess-Control-Allow-Origin в ответе
POST с application/jsonсначала preflight OPTIONS, затем настоящий запроссогласие на метод и Content-Type в preflight
Запрос с Authorizationсначала preflight с Access-Control-Request-HeadersAllowedHeaders, содержащий Authorization
Запрос с cookies (credentials)требует точный Origin вместо звёздочкиточный Origin и AllowCredentials: true

Соберите сервер с CORS

Разложите строки в правильном порядке: роутер, служебный слой, разрешение источников, страховка от паник, маршрут и запуск сервера.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Продакшн-настройка cors.Options

GO
r.Use(cors.Handler(cors.Options{
        AllowedOrigins:   []string{"https://app.example.com"},
        AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
        AllowedHeaders:   []string{"Content-Type", "Authorization", "X-Request-ID"},
        ExposedHeaders:   []string{"X-Request-ID"},
        AllowCredentials: true,
        MaxAge:           300,
}))

Полновесные настройки для реального домена: ExposedHeaders открывает X-Request-ID для JavaScript, MaxAge на 300 секунд кэширует preflight — пять минут браузер не дёргает сервер повторными OPTIONS. OPTIONS в списке методов можно не перечислять: cors.Handler отвечает на preflight сам, но явный метод не помешает, если preflight где-то проверяется ещё и вручную. От localhost до продакшена — замена одного значения в AllowedOrigins; конфигурацию выносят в переменные окружения, чтобы домен менялся без пересборки. Проверяйте всё локальным Go и двумя вкладками браузера: с разрешённого домена и с чужого.

CORS защищает не сервер, а пользователя#

Посыл, который стоит проговорить в конце: выключите CORS — и сервер не пострадает ни на бит. Браузерное правило не действует на curl и вредоносные скрипты вне браузера: они и так стучатся в ваш API без всяких разрешений. CORS нужен для другого: чтобы вкладка с чужого сайта не смогла от имени вашего пользователя — с его cookies — читать ответы вашего API. Поэтому честная настройка CORS — вопрос пользовательской безопасности, а не защиты сервера; защита сервера — это токены, авторизация и предохранители вроде таймаутов из прошлого урока. И обратное следствие: CORS не авторизация. Если часть API рассчитана только на ваш фронтенд, надеяться, что отсутствие заголовков её защитит, — ошибка: любой не-браузерный клиент заголовки игнорирует.

Карточки: CORS

Термин

Что исполняет CORS?

нажмите, чтобы перевернуть
Определение

Браузер. Он проверяет заголовки Access-Control-* в ответе сервера; curl и Postman про CORS ничего не знают.

1 / 5

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • CORS исполняет браузер: без заголовков согласия он прячет ответ от JavaScript.
  • Preflight — OPTIONS с Origin и Access-Control-Request-Method перед непростыми запросами.
  • cors.Handler из github.com/go-chi/cors вешается через r.Use и отвечает на preflight сам.
  • AllowedOrigins — точные домены: звёздочка несовместима с AllowCredentials.
  • ExposedHeaders открывает кастомные заголовки ответа, MaxAge кэширует preflight.
  • Ошибка CORS чинится на сервере, хотя видна только в консоли браузера.
  • CORS не авторизация: серверный клиент его игнорирует.

Домашнее задание

Подключите cors.Handler к сервису из прошлых уроков: разрешите Origin фронтенда с портом, методы GET и POST и заголовок Content-Type. Проверьте curl-ом preflight на POST с application/json, затем уберите домен из AllowedOrigins и убедитесь, что заголовки согласия исчезли, а fetch в браузере упадёт с ошибкой CORS.

Личные заметкиПусто
Хранятся только в вашем браузере (Chi · CORS: чиним браузерные запросы)

Вопросы и ответы по уроку

Можно ли писать Access-Control-Allow-Origin вручную в хендлерах?

Технически да: w.Header().Set("Access-Control-Allow-Origin", ...) в каждом хендлере. Но preflight-OPTIONS придётся обрабатывать отдельно, а один новый домен превращается в правку десятков мест. cors.Handler делает то же самое централизованно.

Почему в Postman нет ошибок CORS, а в браузере есть?

CORS исполняет только браузер: Postman, curl и серверные клиенты отправляют запросы без проверок. Если Postman возвращает данные, а fetch — ошибку, сервер работает, но не отдал заголовков согласия. Чинить нужно cors.Options.

Нужен ли отдельный маршрут для OPTIONS?

Нет: cors.Handler перехватывает preflight до маршрутизации и отвечает сам. Отдельный r.Options нужен только без middleware или когда на preflight должна быть особая логика — обычно это не требуется.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Redis25 уроков~12 чпрактика25 уроков уже доступны

Redis в Go: кэш, сессии и очереди на go-redis

Курс Redis для Go-разработчиков: кэширование запросов, сессии, счётчики, Pub/Sub, Streams и rate limiter. 25 уроков с примерами кода go-redis v9.

  • Подключаться к Redis из Go и настраивать пул соединений
  • Кэшировать результаты запросов и придумывать TTL
  • Работать с хэшами, списками, множествами и ZSET
  • Строить сессии, счётчики, лидерборды и rate limiter