Files
2026-10-23 22:31:02 -03:00

101 lines
2.7 KiB
Go

// 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)
}