Перенаправления
HTTPC по умолчанию автоматически следует HTTP-перенаправлениям (до 10 раз) и записывает полную цепочку. На этой странице — управление следованием, лимиты числа переходов, отслеживание цепочки, семантика кодов состояния, удаление междоменных учётных данных, детекция циклических перенаправлений, доменный белый список и взаимодействие с SSRF-защитой.
Поведение по умолчанию
Без какой-либо конфигурации клиент следует перенаправлениям, пока не будет получен итоговый ответ или не достигнут лимит:
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) задаёт политику перенаправлений для всего клиента:
cfg := httpc.DefaultConfig()
cfg.Defaults.FollowRedirects = true // по умолчанию: следовать
cfg.Defaults.MaxRedirects = 5 // по умолчанию: 10
client, err := httpc.New(cfg)На уровне запроса
WithFollowRedirects / WithMaxRedirects переопределяют конфигурацию клиента для одного запроса:
// Запретить следование только для этого запроса и получить 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).
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 дополнительная обработка не нужна — токен отправляется только исходному хосту и не передаётся третьим доменам при переходах.
Детекция циклических перенаправлений
Помимо лимита числа переходов движок детектирует циклические перенаправления (цель перехода уже встречалась в цепочке ранее). При обнаружении запрос немедленно завершается ошибкой, не дожидаясь исчерпания лимита:
A → B → A цикл: ошибка circular redirect detected: A
A → A → A один и тот же URL подряд: циклом не считается (сервер может каждый раз возвращать разный ответ)Детекция циклов и лимит MaxRedirects дополняют друг друга: лимит покрывает любые зацикливания, а детекция циклов заранее распознаёт «очевидно ходящие по кругу» цепочки, сокращая лишние запросы.
Классификация ошибок перенаправления
Когда следование отклоняется (превышение лимита, цикл, белый список, блокировка SSRF), запрос завершается ClientError с единым Type = ErrorTypeValidation:
| Исходное сообщение об ошибке | Поле Message | Условие срабатывания |
|---|---|---|
stopped after N redirects | redirect 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-защитой |
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 ограничивает цели перенаправлений доверенными доменами, защищая от атак открытого редиректа:
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-проверка соединительного уровня на каждом переходе действует как обычно.
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 удалены при междоменном переходе (защитное поведение) | Заново пройдите аутентификацию на новом домене или переведите сервер на переходы в пределах одного домена |
Что дальше
- Запросы и ответы - опции запроса и обработка ответов
- Защита от SSRF - подробный разбор SSRF-проверок при перенаправлениях
- Обработка ошибок - классификация ErrorType и сопоставление ошибок
- Справочник конфигурации - RequestDefaults и поля безопасности