Projeto bot pontos
This commit is contained in:
100
backend/internal/httpx/httpx.go
Normal file
100
backend/internal/httpx/httpx.go
Normal file
@@ -0,0 +1,100 @@
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user