Go ReverseProxy добавил лишний путь: проверяем Rewrite и SetURL
Входящий запрос /items?q=tea нужно отправить сервису по адресу /internal/items. После ручного переписывания URL часть запросов получает двойной префикс или теряет query. В коде прокси одновременно меняются Host, Path и RawQuery, а тест проверяет только ответ 200. Полезнее сначала зафиксировать, что именно должен увидеть backend.
Определить адрес назначения
У ReverseProxy есть Rewrite с ProxyRequest. Входящий запрос находится в In, исходящий — в Out. Метод SetURL задаёт адрес назначения и объединяет его базовый путь с входящим путём. Если у target есть query, она сочетается с query запроса. Это означает, что префикс /internal достаточно указать в target один раз; повторное ручное добавление в Rewrite продублирует его.
Пример запускает backend и сам прокси через httptest. Адрес назначения содержит /internal и параметр fixed=1. Клиент обращается к /items?q=tea. Backend проверяет полученный Path и оба параметра, затем возвращает наблюдаемую строку. Проверка на стороне backend важна: собственный Out.URL в функции Rewrite ещё не показывает результат полного прохода через транспорт.
Rewrite доступен начиная с Go 1.20. Не задавайте одновременно Director и Rewrite для одного ReverseProxy. Для нового кода Rewrite даёт удобную границу: входящие forwarding-заголовки очищаются перед вызовом, а нужные значения можно выставить осознанно. В этом примере идентичность клиента не передаётся, потому что задача ограничена адресом назначения.
Увидеть запрос на backend
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/http/httputil"
"net/url"
)
func main() {
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/internal/items" || r.URL.Query().Get("fixed") != "1" || r.URL.Query().Get("q") != "tea" {
http.Error(w, "wrong target", http.StatusBadRequest)
return
}
io.WriteString(w, r.URL.Path+" "+r.URL.Query().Encode())
}))
defer backend.Close()
target, err := url.Parse(backend.URL + "/internal?fixed=1")
if err != nil {
panic(err)
}
proxy := &httputil.ReverseProxy{
Rewrite: func(request *httputil.ProxyRequest) {
request.SetURL(target)
},
}
gateway := httptest.NewServer(proxy)
defer gateway.Close()
response, err := gateway.Client().Get(gateway.URL + "/items?q=tea")
if err != nil {
panic(err)
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil || response.StatusCode != 200 || string(body) != "/internal/items fixed=1&q=tea" {
panic("unexpected forwarded request")
}
fmt.Println(string(body))
}
Если базовый target не должен передавать параметры, не храните их в адресе конфигурации. Если клиенту запрещено менять fixed, объединение query само по себе не обеспечивает запрет: клиент способен прислать второе значение того же ключа. Нужен явный контракт повторяющихся параметров и настройка Out.URL.RawQuery после проверки входа. Иначе Get может скрыть несколько разных значений.
Проверить параметры и Host
Не формируйте target из произвольного пользовательского URL. Прокси с таким интерфейсом может стать средством доступа к внутренней сети. Обычно backend выбирается из заранее заданного набора, а пользователь управляет только разрешёнными параметрами операции. Корректное объединение пути не заменяет контроль назначения.
Если backend требует исходный Host, выставьте Out.Host осознанно после SetURL. Адрес соединения и HTTP Host — разные поля. Для виртуального хостинга и проверки подписи это может менять поведение. В тесте backend принимает собственный адрес, поэтому дополнительная подмена Host не нужна.
Проверьте завершающий слеш target, пустой путь запроса и параметры с несколькими значениями. Для маршрута, где шлюз удаляет внешний префикс /api, сделайте это отдельным шагом до объединения и проверяйте точный результат. Изменение пути должно иметь одного владельца, иначе каждый слой начнёт «исправлять» адрес повторно.
После проверки маршрута добавьте отказ backend и убедитесь, какой ответ получает клиент. Сетевой прокси вводит ещё одну границу ошибок и времени; успешная передача пути — только первый проверяемый контракт между двумя программами.
Продолжить практику
В курсе микросервисов на Go вы пройдёте от HTTP-вызова другой программы к дедлайнам, совместимым контрактам и отказам. Откройте программу и продолжите тестом, который проверяет фактическое поведение backend и клиента при изменении маршрута или недоступности зависимости.
Продолжить на практике
- Микросервисы на Go: HTTP, PostgreSQL, gRPC, Kafka и Docker ↗
От работающего Go API — к взаимодействию сервисов, которому можно доверять. Разберитесь, как проектировать контракты, сохранять согласованность данных и восстанавливать обработку после сбоев. HTTP, PostgreSQL, gRPC, Kafka и Docker — через код и наглядные разборы.
