// Package httpx reúne utilitários HTTP compartilhados pela API e pelo worker. package httpx import ( "crypto/rand" "encoding/hex" "encoding/json" "log/slog" "net/http" "runtime/debug" "strings" "time" "botpontos/internal/domain" ) // Problem segue o formato application/problem+json (RFC 9457). type Problem struct { Type string `json:"type"` Title string `json:"title"` Status int `json:"status"` Code string `json:"code,omitempty"` Errors []domain.ValidationError `json:"errors,omitempty"` } func WriteJSON(w http.ResponseWriter, status int, value any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(value) } func WriteProblem(w http.ResponseWriter, status int, title, code string) { WriteProblemDetails(w, Problem{Title: title, Status: status, Code: code}) } func WriteProblemDetails(w http.ResponseWriter, p Problem) { if p.Type == "" { p.Type = "about:blank" } w.Header().Set("Content-Type", "application/problem+json; charset=utf-8") w.WriteHeader(p.Status) _ = json.NewEncoder(w).Encode(p) } type statusRecorder struct { http.ResponseWriter status int } func (r *statusRecorder) WriteHeader(status int) { r.status = status r.ResponseWriter.WriteHeader(status) } // RequestLog registra cada requisição com um request_id. Rotas de saúde // ficam em nível debug, porque o healthcheck do Docker as chama o tempo todo. func RequestLog(logger *slog.Logger, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { start := time.Now() requestID := newRequestID() w.Header().Set("X-Request-ID", requestID) rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK} next.ServeHTTP(rec, req) level := slog.LevelInfo if strings.Contains(req.URL.Path, "/health/") { level = slog.LevelDebug } logger.Log(req.Context(), level, "requisição http", "request_id", requestID, "method", req.Method, "path", req.URL.Path, "status", rec.status, "duration_ms", time.Since(start).Milliseconds(), ) }) } // Recover transforma um panic num 500 sem derrubar o processo. func Recover(logger *slog.Logger, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { defer func() { if v := recover(); v != nil { if v == http.ErrAbortHandler { panic(v) } logger.Error("panic no handler", "panic", v, "stack", string(debug.Stack())) WriteProblem(w, http.StatusInternalServerError, "Erro interno", "INTERNAL") } }() next.ServeHTTP(w, req) }) } func newRequestID() string { b := make([]byte, 8) _, _ = rand.Read(b) return hex.EncodeToString(b) }