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 — через код и наглядные разборы.