Go HTTP-клиент превратил POST в GET после redirect: проверяем код перенаправления
Клиент отправил заказ методом POST, получил успешный ответ, но новый заказ не появился. В журнале принимающего приложения виден GET без тела. Между двумя адресами обнаруживается перенаправление: например, шлюз добавляет завершающий слеш или переводит запрос на другой путь. Клиент Go следует перенаправлению автоматически, поэтому последний успешный ответ легко скрывает произошедшее изменение метода.
У разных кодов перенаправления разный договор. При обычном POST ответы 301, 302 и 303 приводят к следующему запросу GET без тела. Ответы 307 и 308 сохраняют метод и тело, если клиент способен повторно прочитать исходные данные. Поэтому исправление зависит от того, какой смысл вложил сервер в новый адрес: повторить операцию или показать страницу с её результатом.
Сначала узнаём промежуточный ответ
Для диагностики временно запретите автоматическое следование через CheckRedirect с возвратом http.ErrUseLastResponse. Тогда клиент получит исходный статус и заголовок Location. Тело такого ответа всё равно нужно закрыть. Если там 302, а конечный обработчик ожидает POST, начните с настройки сервера или используйте канонический адрес сразу.
Не меняйте все 303 на 307 механически. После успешной записи сервер может намеренно направить клиента на чтение созданного ресурса. В этом сценарии GET является ожидаемым продолжением, а отправка того же POST на адрес результата была бы ошибкой. Отделите перенос самой операции от перехода к её представлению. Проверить нужно оба адреса и журналы обработки, а не только финальный код 200.
Воспроизводим поведение клиента
Следующий пример поднимает локальный тестовый сервер. Первый путь возвращает указанный код, второй сообщает, какой метод и тело до него дошли. Это позволяет наблюдать работу настоящего http.Client. Пример использует strings.Reader: при создании запроса Go устанавливает для него GetBody, то есть способ снова получить исходное тело.
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"strconv"
"strings"
)
func main() {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/redirect" {
code, err := strconv.Atoi(r.URL.Query().Get("code"))
if err != nil { http.Error(w, "bad code", 400); return }
w.Header().Set("Location", "/target")
w.WriteHeader(code)
return
}
body, err := io.ReadAll(r.Body)
if err != nil { http.Error(w, "read failed", 400); return }
fmt.Fprintf(w, "%s|%s", r.Method, body)
}))
defer server.Close()
client := server.Client()
for _, code := range []int{301, 302, 303, 307, 308} {
req, err := http.NewRequest("POST", server.URL+"/redirect?code="+strconv.Itoa(code), strings.NewReader("order=17"))
if err != nil { panic(err) }
resp, err := client.Do(req)
if err != nil { panic(err) }
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil { panic(err) }
want := "GET|"
if code == 307 || code == 308 { want = "POST|order=17" }
if string(body) != want { panic(string(body)) }
fmt.Println(code, string(body))
}
req, err := http.NewRequest("POST", server.URL+"/redirect?code=307", strings.NewReader("order=17"))
if err != nil { panic(err) }
req.GetBody = nil
resp, err := client.Do(req)
if err != nil { panic(err) }
resp.Body.Close()
if resp.StatusCode != 307 { panic("unexpected replay without GetBody") }
fmt.Println("without GetBody:", resp.StatusCode)
}
Сохраните файл и выполните go run main.go. Для 301, 302 и 303 тест получает GET с пустым телом, для 307 и 308 — POST с исходными данными. Последняя проверка убирает GetBody при непустом теле. Клиент уже не может воспроизвести его и возвращает ответ 307, не отправляя запрос на конечный путь. Это существенный случай для потоковой загрузки из файла или другого невоспроизводимого источника.
Если тело собирается из потока, заранее решите, допустимо ли хранить его повторяемую копию. Для небольшого JSON подойдут буфер или строка. Для большого файла можно заново открыть источник, но его содержимое должно оставаться тем же, а управление закрытием — быть явным. Наличие функции повторного чтения ещё не делает повторную бизнес-операцию безопасной: это только техническая возможность передать байты снова.
Исправляем маршрут и оставляем проверку
Для API создания заказа предпочтителен прямой запрос к окончательному адресу. Если перенос обязателен и операция должна сохраниться, серверу подходит 307 или 308 с учётом временности изменения. Добавьте интеграционный тест с реальным путём через шлюз: проверьте метод, содержимое и число созданных записей. Локальный пример объясняет клиент, а такая проверка выявит дополнительное перенаправление на вашей инфраструктуре.
Политику переходов полезно ограничить и по адресу назначения: путь из Location может вести на другой хост. Технические правила Go собраны в описании Client.Do. Для изменяющего запроса отдельно определите, как узнать результат после сетевого обрыва: повтор после неизвестного исхода рассматривается в материале о безопасных HTTP-повторах.
При первом расследовании сопоставьте время запроса, адрес и идентификатор операции на обоих обработчиках. Один только журнал конечного пути показывает уже изменённый метод. Если цепочка состоит из нескольких переходов, проверьте каждый статус по очереди: сохранение метода на первом шаге не исключает его изменения на втором. После исправления оставьте проверку в клиентском тесте, чтобы новая настройка шлюза не вернула прежнее поведение. Ожидаемый маршрут должен быть частью договора между командами.
В платном курсе «Микросервисы на Go» — посмотрите программу вы продолжите работу с HTTP-контрактами, дедлайнами, неизвестным исходом POST и защитой от дублирования результата. После разбора перенаправлений логичный следующий шаг — связать сетевое поведение клиента с ограничениями базы и проверками отказов. Тогда успешный ответ будет означать понятный результат операции, а не просто завершение цепочки запросов.
Продолжить на практике
- Микросервисы на Go: HTTP, PostgreSQL, gRPC, Kafka и Docker ↗
От работающего Go API — к взаимодействию сервисов, которому можно доверять. Разберитесь, как проектировать контракты, сохранять согласованность данных и восстанавливать обработку после сбоев. HTTP, PostgreSQL, gRPC, Kafka и Docker — через код и наглядные разборы.
