Avatar ·

Как установить HTTP-заголовки в Go (CORS, Content-Type и другие)

Всем привет! Я начинающий Go-разработчик и пишу свой первый REST API на net/http. Столкнулся с проблемой: мой фронтенд (React, запущенный на localhost:3000) не может отправлять запросы к серверу (localhost:8080) из-за CORS. Я пытаюсь установить заголовки вручную, но что-то идет не так. Вот мой код: ```go package main import ( "fmt" "net/http" ) func handler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, `{"message": "Hello"}`) } func main() { http.HandleFunc("/api", handler) http.ListenAndServe(":8080", nil) } ``` Проблема в том, что когда я делаю POST-запрос с телом JSON, браузер все равно выдает ошибку CORS. Я пробовал добавлять заголовки в разных местах — до и после записи ответа — но результат тот же. Также я читал про middleware, но не понимаю, как их применять для всех маршрутов сразу. Подскажите, как правильно устанавливать HTTP-заголовки в Go? Может, есть какой-то стандартный способ для CORS? И почему мой код не работает для preflight-запросов (OPTIONS)? Заранее спасибо! <<>>

Проблема: CORS и порядок установки заголовков

Ваш код не работает по двум основным причинам:

  1. Preflight-запросы (OPTIONS) — браузер перед реальным POST-запросом отправляет OPTIONS-запрос, чтобы проверить, разрешен ли CORS. Ваш сервер не обрабатывает этот метод.
  2. Порядок заголовков — в Go заголовки нужно устанавливать до вызова Write() или WriteHeader(). После записи тела ответа заголовки игнорируются.

Правильное решение: middleware для CORS

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

package main

import (
    "encoding/json"
    "log"
    "net/http"
)

// corsMiddleware добавляет CORS-заголовки ко всем ответам
func corsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Устанавливаем заголовки ДО обработки запроса
        w.Header().Set("Access-Control-Allow-Origin", "*")
        w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
        w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
        w.Header().Set("Access-Control-Max-Age", "86400") // кэширование preflight на 24 часа

        // Обработка preflight-запроса (OPTIONS)
        if r.Method == http.MethodOptions {
            w.WriteHeader(http.StatusOK)
            return
        }

        // Передаем управление следующему обработчику
        next.ServeHTTP(w, r)
    })
}

// apiHandler — ваш основной обработчик
func apiHandler(w http.ResponseWriter, r *http.Request) {
    // Важно: заголовки уже установлены в middleware!
    w.Header().Set("Content-Type", "application/json")
    
    response := map[string]string{"message": "Hello"}
    json.NewEncoder(w).Encode(response)
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/api", apiHandler)

    // Оборачиваем весь mux в middleware
    handler := corsMiddleware(mux)

    log.Println("Server started on :8080")
    log.Fatal(http.ListenAndServe(":8080", handler))
}

Ключевые моменты

  • Middleware — функция, которая оборачивает ваш обработчик и выполняется для каждого запроса.
  • Preflight (OPTIONS) — обязательно обрабатывайте этот метод и возвращайте 200 OK без тела.
  • Порядок заголовков: все Header().Set() должны быть вызваны до WriteHeader() или Write(). В middleware это происходит до вызова next.ServeHTTP().
  • Content-Type — если вы используете json.NewEncoder, он сам установит правильный Content-Type. Если пишете вручную через fmt.Fprintf, ставьте заголовок явно.

Более гибкий вариант: обработка конкретных origin

Если вам нужно разрешить только определенные домены, а не *:

func corsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        origin := r.Header.Get("Origin")
        allowedOrigins := map[string]bool{
            "http://localhost:3000": true,
            "https://mysite.com":    true,
        }

        if allowedOrigins[origin] {
            w.Header().Set("Access-Control-Allow-Origin", origin)
        }

        // остальная логика...
    })
}

Также рекомендую использовать готовые библиотеки, например rs/cors, если проект станет больше — они уже учитывают все edge cases.

<<>>

Альтернатива: использование библиотеки rs/cors

Если вы не хотите писать middleware вручную, есть отличная библиотека github.com/rs/cors. Она уже решает все проблемы с CORS, включая preflight, заголовки и безопасность.

Установка:

go get github.com/rs/cors

Пример использования:

package main

import (
    "encoding/json"
    "log"
    "net/http"

    "github.com/rs/cors"
)

func apiHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]string{"message": "Hello"})
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/api", apiHandler)

    // Настройка CORS через библиотеку
    c := cors.New(cors.Options{
        AllowedOrigins:   []string{"http://localhost:3000", "https://mysite.com"},
        AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
        AllowedHeaders:   []string{"Content-Type", "Authorization"},
        AllowCredentials: true,
        MaxAge:           86400,
    })

    handler := c.Handler(mux)
    log.Fatal(http.ListenAndServe(":8080", handler))
}

Библиотека автоматически:

  • Обрабатывает OPTIONS-запросы
  • Устанавливает правильные заголовки
  • Поддерживает сложные сценарии (credentials, Vary header)

Это упрощает код и снижает риск ошибок. Для большинства проектов рекомендую именно этот подход.