101 lines
2.7 KiB
Go
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)
|
|
}
|