Опирается на правила:
R-HDR-1..4,R-HDR-X1→ раздел Заголовки.
Важно знать
- Стандартные заголовки используются по назначению:
Content-Type,Authorization,Location.- Кастомные — с доменным префиксом, без
X-(Shop-Request-Id, неX-Request-Id).Idempotency-Key— для неидемпотентныхPOSTс бизнес-эффектом.traceparent— W3C Trace Context; интегрируется через OTelotelhttp.traceIdв теле ошибки — извлекается из OTel контекста в хендлере.X-префикс в кастомных заголовках — запрещён (RFC 6648 устарел).
Стандартные заголовки
R-HDR-1: использовать по назначению.
// Content-Type на запрос/ответ
w.Header().Set("Content-Type", "application/json")
// Authorization — читаем из запроса
token := r.Header.Get("Authorization") // "Bearer <token>"
// Accept-Language — для локализации (R-LOC-1)
lang := r.Header.Get("Accept-Language")
// Location — при создании ресурса (R-RSP-3)
w.Header().Set("Location", "/api/v1/orders/"+orderID)
Кастомные заголовки
R-HDR-2: доменный префикс, без X-.
Префикс выбирается один раз для всех сервисов проекта и фиксируется в стандартах команды:
const headerRequestID = "Shop-Request-Id"
const headerVersion = "Shop-Api-Version"
func requestIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestID := r.Header.Get(headerRequestID)
if requestID == "" {
requestID = uuid.New().String()
}
w.Header().Set(headerRequestID, requestID)
ctx := context.WithValue(r.Context(), ctxRequestID{}, requestID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Idempotency-Key
R-HDR-3: для POST с бизнес-эффектом (оплата, создание заказа).
func createOrder(w http.ResponseWriter, r *http.Request) {
idempotencyKey := r.Header.Get("Idempotency-Key")
if idempotencyKey == "" {
httperr.Write(w, r, apperr.NewValidation("Idempotency-Key header required"))
return
}
// проверяем кеш по ключу
if cached, ok := idempotencyStore.Get(idempotencyKey); ok {
writeJSON(w, http.StatusOK, cached)
return
}
var req CreateOrderRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
httperr.Write(w, r, apperr.NewValidation("invalid request body"))
return
}
order, err := svc.CreateOrder(r.Context(), toCreateOrderCommand(req))
if err != nil {
httperr.Write(w, r, err)
return
}
resp := toOrderResponse(order)
idempotencyStore.Set(idempotencyKey, resp, 24*time.Hour)
w.Header().Set("Location", "/api/v1/orders/"+order.ID)
writeJSON(w, http.StatusCreated, resp)
}
Хранилище ключей — Redis или in-memory с TTL. Если ключ уже использован и запрос идентичен — возвращаем кешированный ответ. Если ключ совпадает, но тело другое — 409 Conflict.
W3C Trace Context — traceparent
R-HDR-4: OTel middleware автоматически читает traceparent из запроса и проставляет его в контекст.
import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
func main() {
r := chi.NewRouter()
r.Use(otelhttp.NewMiddleware("order-service")) // traceparent → context
// ...
}
Извлечение traceId из контекста для подстановки в ProblemDetails:
import (
"go.opentelemetry.io/otel/trace"
)
func traceIDFromCtx(ctx context.Context) string {
span := trace.SpanFromContext(ctx)
if !span.SpanContext().IsValid() {
return ""
}
return span.SpanContext().TraceID().String()
}
Использование в writeProblem:
func writeProblem(w http.ResponseWriter, status int, code, title, detail string, traceID string) {
p := ProblemDetails{
Type: problemType(code),
Title: title,
Status: status,
Detail: detail,
Code: code,
TraceID: traceID,
}
w.Header().Set("Content-Type", "application/problem+json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(p)
}
// в хендлере или httperr.Write:
httperr.Write(w, r, err)
// внутри: traceID := traceIDFromCtx(r.Context())
Прокидывание traceparent в исходящих запросах
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
)
func callDownstream(ctx context.Context, url string) (*http.Response, error) {
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
// инжектируем traceparent + tracestate в исходящий запрос
otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header))
return http.DefaultClient.Do(req)
}
Сборка middleware-стека
r := chi.NewRouter()
r.Use(
otelhttp.NewMiddleware("order-service"), // traceparent (R-HDR-4)
requestIDMiddleware, // Shop-Request-Id (R-HDR-2)
RateLimitMiddleware(globalLimiter), // Retry-After (R-RATE-1)
Recoverer, // паника → 500
)
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
w.Header().Set("X-Request-Id", ...) | R-HDR-X1 | Shop-Request-Id (без X-) |
w.Header().Set("X-Trace-Id", ...) | R-HDR-X1 | OTel traceparent (W3C стандарт) |
| Idempotency-Key без проверки кеша | R-HDR-3 | хранить результат и возвращать при повторе |
| Кастомный propagation вместо W3C | R-HDR-4 | otelhttp.NewMiddleware |
r.Header.Get("Authorization") без проверки | R-HDR-1 | middleware извлекает и кладёт в контекст |
Куда дальше
- go/errors.md —
traceIdв теле ошибки. - go/rate-limiting-files-deprecation.md —
Retry-After,RateLimit-*,Sunset. - go/json-and-responses.md —
Locationпри создании. - go/batch-async-localization.md —
Accept-Language.