Слеш внутри ID Go API: как сохранить один сегмент URL
Хранилище выдаёт ключ invoice/2026, а клиент должен получить его по endpoint /objects/{key}. Ручная склейка создаёт /objects/invoice/2026: сервер видит два сегмента вместо одного идентификатора. Query-кодирование тоже не подходит, потому что специальные символы пути и параметры запроса имеют разные правила.
Разделить путь и значение сегмента
url.URL хранит Path в декодированном виде. Для сохранения специального представления есть RawPath: это необязательная закодированная форма того же пути. Если положить строку с %2F прямо в Path, при сериализации знак процента может быть закодирован повторно, и сервер получит буквальный текст %2F вместо слеша внутри значения.
В примере базовый путь известен и контролируется приложением. Ключ добавляется к Path как данные, а RawPath строится из того же основания и PathEscape(key). Метод EscapedPath проверяет, соответствует ли RawPath декодированному пути, и использует подходящее представление при формировании запроса. На сервере мы читаем исходную форму RequestURI и декодированный URL.Path: они показывают разные уровни одного запроса.
Такое представление не гарантирует, что любой маршрутизатор пропустит закодированный слеш как один параметр. Прокси и роутеры могут нормализовать путь раньше обработчика. Поэтому нужно проверять весь маршрут конкретного API. Локальный пример фиксирует поведение стандартного клиента и сервера, а контракт внешнего поставщика определяет, разрешён ли подобный ключ вообще.
Проверить RequestURI сервера
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
)
func main() {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.RequestURI != "/objects/invoice%2F2026" {
panic("encoded segment changed: " + r.RequestURI)
}
if r.URL.Path != "/objects/invoice/2026" {
panic("decoded path changed")
}
io.WriteString(w, r.RequestURI)
}))
defer server.Close()
u, err := url.Parse(server.URL)
if err != nil {
panic(err)
}
key := "invoice/2026"
u.Path = "/objects/" + key
u.RawPath = "/objects/" + url.PathEscape(key)
response, err := server.Client().Get(u.String())
if err != nil {
panic(err)
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil || string(body) != "/objects/invoice%2F2026" {
panic("unexpected request target")
}
fmt.Println(string(body))
}
Не применяйте PathEscape ко всему готовому пути: структурные разделители тоже превратятся в данные. Экранируйте именно значение одного сегмента. Если identifier по правилам сервиса может содержать только цифры и буквы, проверка допустимого набора на границе будет проще. Если слеши являются частью иерархического пути, их, наоборот, нужно сохранить как разделители и отдельно решить вопрос сегментов точка и точка-точка.
Проверить посредников и другие символы
url.JoinPath удобен для соединения частей пути, но он очищает структурный путь. Поэтому этот инструмент не заменяет решение, какие символы принадлежат структуре, а какие — одному непрозрачному ID. В статье о query уже разобрана похожая граница для амперсанда и плюса; здесь проверяется другая часть URL и другое поведение сервера.
Добавьте ключ с пробелом, знаком процента и кириллицей. Для каждого значения проверьте исходный RequestURI и ожидаемое декодирование. Не сравнивайте только красивую строку, которую вы напечатали перед отправкой: клиент или посредник может изменить представление. Для подписи запросов используйте ту каноническую форму, которую требует поставщик.
Если этим клиентом управляет пользователь, фиксируйте разрешённые scheme и host отдельно. Корректное представление одного ID не ограничивает выбор сетевого адреса. Здесь сервер задаётся самим тестом, поэтому проверка касается исключительно сохранения значения при переходе в HTTP-путь.
Продолжить практику
Сохранность пути — часть контракта клиента и обработчика. В курсе «Микросервисы на Go» вы проверите HTTP-взаимодействие двух программ и обработку неправильных ответов. Откройте программу и продолжите контрактным тестом, который проверяет не только статус, но и фактически полученный идентификатор.
Продолжить на практике
- Микросервисы на Go: HTTP, PostgreSQL, gRPC, Kafka и Docker ↗
От работающего Go API — к взаимодействию сервисов, которому можно доверять. Разберитесь, как проектировать контракты, сохранять согласованность данных и восстанавливать обработку после сбоев. HTTP, PostgreSQL, gRPC, Kafka и Docker — через код и наглядные разборы.
