// Package httpx — общие HTTP-утилиты: ответы об ошибках (RFC 9457), логирование, middleware. package httpx import ( "encoding/json" "errors" "log/slog" "net/http" "strconv" "pipu/api/internal/platform/apperr" ) // Problem — тело ответа application/problem+json. Поле Code переводится клиентом через i18n. type Problem struct { Type string `json:"type"` Status int `json:"status"` Code string `json:"code"` Detail string `json:"detail,omitempty"` Params map[string]any `json:"params,omitempty"` Errors []apperr.FieldError `json:"errors,omitempty"` } // WriteProblem пишет ответ об ошибке. func WriteProblem(w http.ResponseWriter, status int, code, detail string) { writeProblem(w, Problem{Type: "about:blank", Status: status, Code: code, Detail: detail}) } func writeProblem(w http.ResponseWriter, p Problem) { w.Header().Set("Content-Type", "application/problem+json") w.WriteHeader(p.Status) _ = json.NewEncoder(w).Encode(p) } // WriteError пишет доменную ошибку *apperr.Error; любую другую — как 500 (подробности в лог). func WriteError(log *slog.Logger, w http.ResponseWriter, r *http.Request, err error) { var ae *apperr.Error if !errors.As(err, &ae) { log.ErrorContext(r.Context(), "handler failed", "path", r.URL.Path, "err", err) WriteProblem(w, http.StatusInternalServerError, "common.internal", "internal error") return } if ae.RetryAfter > 0 { w.Header().Set("Retry-After", strconv.Itoa(ae.RetryAfter)) } writeProblem(w, Problem{ Type: "about:blank", Status: ae.Status, Code: ae.Code, Detail: ae.Detail, Params: ae.Params, Errors: ae.Fields, }) } // NotFound — обработчик неизвестных путей. func NotFound(w http.ResponseWriter, _ *http.Request) { WriteProblem(w, http.StatusNotFound, "common.not_found", "route not found") } // MethodNotAllowed — обработчик неподдерживаемых методов. func MethodNotAllowed(w http.ResponseWriter, _ *http.Request) { WriteProblem(w, http.StatusMethodNotAllowed, "common.method_not_allowed", "method not allowed") } // RequestError — ошибка разбора запроса (strict-server). func RequestError(w http.ResponseWriter, _ *http.Request, err error) { WriteProblem(w, http.StatusBadRequest, "common.bad_request", err.Error()) } // ResponseError — ошибка обработчика: доменные — как есть, прочие — 500 с деталями только в логе. func ResponseError(log *slog.Logger) func(http.ResponseWriter, *http.Request, error) { return func(w http.ResponseWriter, r *http.Request, err error) { WriteError(log, w, r, err) } }