Skip to content

Перенаправления

HTTPC по умолчанию автоматически следует HTTP-перенаправлениям (до 10 раз) и записывает полную цепочку. На этой странице — управление следованием, лимиты числа переходов, отслеживание цепочки, семантика кодов состояния, удаление междоменных учётных данных, детекция циклических перенаправлений, доменный белый список и взаимодействие с SSRF-защитой.

Поведение по умолчанию

Без какой-либо конфигурации клиент следует перенаправлениям, пока не будет получен итоговый ответ или не достигнут лимит:

go
package main

import (
    "fmt"
    "log"

    "github.com/cybergodev/httpc"
)

func main() {
    result, err := httpc.Get("https://httpbin.org/redirect/2")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(result.StatusCode())        // 200 (код итогового ответа)
    fmt.Println(result.Meta.RedirectCount)  // 2 (фактически выполненные перенаправления)
}

Движок автоматически следует пяти кодам состояния перенаправления — 301/302/303/307/308; семантика методов соответствует спецификации HTTP:

КодОбработка метода
301 / 302 / 303Может переписать POST в GET (допускается спецификацией)
307 / 308Повторная отправка с исходным методом и телом

Семантика кодов состояния (301/302/303/307/308)

Различия пяти кодов по «перезаписи метода» и «телу запроса»:

КодЗначениеОбработка методаТело запросаТипичное применение
301Перемещён навсегдаGET/HEAD без изменений; POST и др. переписываются в GETОтбрасываетсяМиграция доменов, нормализация URL
302Временно перемещён (Found)Как 301 (фактически сложившийся стандарт)ОтбрасываетсяВременные переходы, редирект после входа
303Смотреть другое (See Other)Всегда переписывается в GETОтбрасываетсяПереход на страницу результата после POST
307Временное перенаправлениеМетод сохраняетсяПовторно отправляетсяВременный переход с повторной отправкой body
308Постоянное перенаправлениеМетод сохраняетсяПовторно отправляетсяПостоянный переход с повторной отправкой body

307/308 с телом запроса не следуются автоматически

307/308 требуют повторной отправки тела исходным методом. Базовый net/http следует перенаправлению только когда тело воспроизводимо (установлен GetBody), а HTTPC при построении запроса эту функцию не устанавливает — поэтому запросы с непустым телом при получении 307/308 не следуют автоматически: 3xx-ответ возвращается как есть (без ошибки). Чтобы следовать таким переходам, отключите следование через WithFollowRedirects(false) и обработайте цикл вручную или переведите сервер на 302/303.

300/304 и другие 3xx вне области следования

Даже с заголовком Location движок следует только 301/302/303/307/308; 300 (Multiple Choices), 304 (Not Modified) и прочие 3xx-ответы возвращаются как есть — их обрабатывает вызывающая сторона. Учтите, что Result.IsRedirect() проверяет весь диапазон 300–399 и не связан с автоматическим следованием.

Превышение лимита — ошибка

Когда число перенаправлений превышает MaxRedirects, запрос завершается ошибкой (сообщение вида stopped after 3 redirects) — бесконечного цикла не будет. Допустимый диапазон MaxRedirects — 0–50; значения вне диапазона вызывают ошибку при валидации конфигурации.

Управление следованием

Конфигурация перенаправлений распределена по трём уровням — от большего охвата к меньшему:

УровеньКонфигурацияОбласть действия
КлиентаConfig.Defaults.FollowRedirects / MaxRedirectsВсе запросы клиента
ЗапросаWithFollowRedirects(bool) / WithMaxRedirects(n)Один запрос, переопределяет конфигурацию клиента
ПресетыSecureConfig(), MinimalConfig() (обе ставят FollowRedirects=false)Для безопасных/минимальных сценариев следование отключено по умолчанию

На уровне клиента

Config.Defaults (RequestDefaults) задаёт политику перенаправлений для всего клиента:

go
cfg := httpc.DefaultConfig()
cfg.Defaults.FollowRedirects = true  // по умолчанию: следовать
cfg.Defaults.MaxRedirects = 5        // по умолчанию: 10

client, err := httpc.New(cfg)

На уровне запроса

WithFollowRedirects / WithMaxRedirects переопределяют конфигурацию клиента для одного запроса:

go
// Запретить следование только для этого запроса и получить 3xx напрямую
result, err := httpc.Get("https://httpbin.org/redirect/1",
    httpc.WithFollowRedirects(false),
)
if result.IsRedirect() {
    fmt.Println(result.Response.Headers.Get("Location")) // адрес цели перенаправления
}

// Ограничить следование тремя переходами только для этого запроса
result, err = httpc.Get(url, httpc.WithMaxRedirects(3))

MaxRedirects(0) — не отключение

0 — сигнальное значение «не задано»: движок возвращается к умолчанию 10, а не отключает перенаправления. Чтобы отключить следование, используйте WithFollowRedirects(false) или Config.Defaults.FollowRedirects = false.

SecureConfig по умолчанию запрещает перенаправления

Пресет SecureConfig() устанавливает FollowRedirects = false, не позволяя уводить запросы на внутренние адреса через перенаправления (SSRF через редирект). Подробности — в Защите от SSRF.

Отслеживание цепочки перенаправлений

Result.Meta фиксирует информацию о перенаправлениях для каждого запроса:

ПолеОписание
Meta.RedirectCountЧисло фактически выполненных перенаправлений
Meta.RedirectChainПоследовательность URL, пройденных при перенаправлениях

Точная семантика обоих полей:

  • RedirectChain фиксирует URL-источник каждого перехода: первый элемент — URL исходного запроса, далее промежуточные URL по порядку; итоговый целевой URL в цепочку не входит. Когда нужен конечный адрес, читайте result.Request.URL (итоговый URL запроса после завершения следования).
  • RedirectCount всегда равен len(RedirectChain).
go
package main

import (
    "fmt"
    "log"

    "github.com/cybergodev/httpc"
)

func main() {
    result, err := httpc.Get("https://httpbin.org/redirect/3")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Выполнено перенаправлений: %d\n", result.Meta.RedirectCount)
    for i, u := range result.Meta.RedirectChain {
        fmt.Printf("  %d. %s\n", i+1, u)
    }
    fmt.Println("Итоговый адрес:", result.Request.URL)
    // Вывод:
    //   1. https://httpbin.org/redirect/3
    //   2. https://httpbin.org/redirect/2
    //   3. https://httpbin.org/redirect/1
    // Итоговый адрес: https://httpbin.org/get
}

Автоудаление междоменных учётных данных

При следовании перенаправлениям движок на каждом переходе проверяет целевое имя хоста: если оно не совпадает с именем хоста исходного запроса (междоменный переход), чувствительные заголовки автоматически удаляются, чтобы учётные данные не утекли цели перенаправления:

ЗаголовокЦелевой хост = хост исходного запросаЦелевой хост ≠ хост исходного запроса
AuthorizationСохраняетсяУдаляется
Proxy-AuthorizationСохраняетсяУдаляется
CookieСохраняетсяУдаляется
Другие пользовательские заголовкиСохраняютсяСохраняются

Детали определения:

  • База сравнения — имя хоста исходного запроса (via[0] первого перехода), а не предыдущего перехода. api.example.com → www.example.com считается междоменным переходом (точное сравнение имён хостов: другие поддомены тоже считаются); возврат A → B → A на исходный хост удалением не сопровождается.
  • Удаление не зависит от cookie jar: даже если EnableCookies не включён, вручную установленный заголовок Cookie при междоменном переходе тоже удаляется.

С WithBasicAuth / WithBearerToken дополнительная обработка не нужна — токен отправляется только исходному хосту и не передаётся третьим доменам при переходах.

Детекция циклических перенаправлений

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

text
A → B → A     цикл: ошибка circular redirect detected: A
A → A → A     один и тот же URL подряд: циклом не считается (сервер может каждый раз возвращать разный ответ)

Детекция циклов и лимит MaxRedirects дополняют друг друга: лимит покрывает любые зацикливания, а детекция циклов заранее распознаёт «очевидно ходящие по кругу» цепочки, сокращая лишние запросы.

Классификация ошибок перенаправления

Когда следование отклоняется (превышение лимита, цикл, белый список, блокировка SSRF), запрос завершается ClientError с единым Type = ErrorTypeValidation:

Исходное сообщение об ошибкеПоле MessageУсловие срабатывания
stopped after N redirectsredirect limit exceededЧисло переходов достигло MaxRedirects (или значения по умолчанию 10)
circular redirect detected: <URL>circular redirect detectedЦелевой URL уже встречался в цепочке переходов
redirect blocked by whitelist: ...redirect blocked by policyЦелевой домен не входит в RedirectWhitelist
redirect blocked: ...redirect blocked by policyЦелевой хост заблокирован SSRF-защитой
go
package main

import (
    "errors"
    "fmt"
    "log"

    "github.com/cybergodev/httpc"
)

func main() {
    cfg := httpc.DefaultConfig()
    cfg.Defaults.MaxRedirects = 2 // разрешаем только 2 перенаправления

    client, err := httpc.New(cfg)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    // /redirect/5 требует 5 переходов — неизбежно превысит лимит
    _, err = client.Get("https://httpbin.org/redirect/5")
    if err != nil {
        var clientErr *httpc.ClientError
        if errors.As(err, &clientErr) && clientErr.Type == httpc.ErrorTypeValidation {
            fmt.Println("Перенаправление отклонено:", clientErr.Message)
            // Вывод: Перенаправление отклонено: redirect limit exceeded
        }
    }
}

SSRF-проверка цели перенаправления включает ещё два жёстких правила: разрешены только протоколы http/https (Location с ftp:// и кастомными протоколами отклоняется) и целевой хост не должен быть пустым. На этапе валидации DNS-разрешение не выполняется — полная проверка IP и защита от DNS rebinding выполняются диалером (dialer) в момент установления соединения (подробнее — в Защите от SSRF).

Доменный белый список

Security.RedirectWhitelist ограничивает цели перенаправлений доверенными доменами, защищая от атак открытого редиректа:

go
cfg := httpc.DefaultConfig()
cfg.Security.RedirectWhitelist = []string{
    "api.example.com",
    "*.cdn.example.com", // wildcard: строгие поддомены, голый домен не входит
}

client, err := httpc.New(cfg)

Детали правил сопоставления:

  • Точное совпадение: api.example.com совпадает только с самим собой.
  • Wildcard: *.cdn.example.com совпадает со строгими поддоменами (например, img.cdn.example.com), но не с голым доменом cdn.example.com; чтобы разрешить и то и другое, перечислите оба.
  • Сравнивается имя хоста цели Location (без порта и протокола); перед сравнением выполняется нормализация: пробелы по краям игнорируются, регистр не учитывается.

После настройки запросы с перенаправлением на домены вне белого списка отклоняются; цели перенаправлений дополнительно проходят SSRF IP-проверку. Работая с URL от пользователей, используйте SecureConfig или белый список.

Ручная обработка перенаправлений

Если нужны проверка каждого перехода, условное следование или собственное логирование — отключите автоматическое следование и обработайте цикл вручную. Учтите два момента: Location может быть относительным адресом, его нужно разрешить в абсолютный относительно текущего URL; ручной цикл не проходит проверку белого списка, но SSRF IP-проверка соединительного уровня на каждом переходе действует как обычно.

go
package main

import (
    "fmt"
    "log"
    "net/url"

    "github.com/cybergodev/httpc"
)

func main() {
    cfg := httpc.DefaultConfig()
    cfg.Defaults.FollowRedirects = false

    client, err := httpc.New(cfg)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    currentURL := "https://httpbin.org/redirect/3"
    base, err := url.Parse(currentURL)
    if err != nil {
        log.Fatal(err)
    }

    for i := 0; i < 5; i++ {
        result, err := client.Get(currentURL)
        if err != nil {
            log.Fatal(err)
        }
        if !result.IsRedirect() {
            fmt.Println("Достигнут итоговый адрес:", currentURL)
            break
        }

        location := result.Response.Headers.Get("Location")
        if location == "" {
            fmt.Println("В ответе-перенаправлении нет заголовка Location, прекращаем следование")
            break
        }

        // Относительный адрес разрешается в абсолютный относительно текущего URL
        next, err := base.Parse(location)
        if err != nil {
            log.Fatal(err)
        }
        fmt.Printf("Переход %d: %s\n", i+1, next.String())

        currentURL = next.String()
        base = next
    }
}

Ответственность за безопасность в ручном цикле

Белый список и предпроверка целей перенаправления из автоматического следования на ручной цикл не действуют. Обрабатывая Location из недоверенного источника, проверяйте целевой домен в цикле сами (или переиспользуйте логику белого списка); SSRF-проверка соединительного уровня остаётся страховкой, но контроль на уровне доменов — ваша задача.

Типичные проблемы

СимптомПричинаРешение
POST получил 307/308 и не последовалТело запроса невоспроизводимо (GetBody не установлен), по спецификации автоматически не следуетРучной цикл или переход сервера на 302/303
WithMaxRedirects(0) не отключил следование0 — сигнальное значение «не задано», откат к умолчанию 10Используйте WithFollowRedirects(false)
300/399 с Location не выполнили переходДвижок следует только 301/302/303/307/308Обрабатывайте сами через IsRedirect() + Location
Параметры запроса «потерялись» после переходаЦелевая строка запроса полностью определяется Location, клиент не объединяет параметры исходного запросаСервер должен включить нужные параметры в Location
Нужно узнать, на каком домене оказалисьRedirectChain не содержит итоговый URLЧитайте result.Request.URL
401 после междоменного переходаAuthorization/Cookie удалены при междоменном переходе (защитное поведение)Заново пройдите аутентификацию на новом домене или переведите сервер на переходы в пределах одного домена

Что дальше