112 lines
3.1 KiB
Go
112 lines
3.1 KiB
Go
// Package middleware provides chi-compatible HTTP middleware:
|
|
// request-id, panic recovery, structured logging, CORS.
|
|
package middleware
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"runtime/debug"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
type ctxKey string
|
|
|
|
const ctxKeyRequestID ctxKey = "request_id"
|
|
|
|
// RequestID assigns a uuid v4 to each request and stores it in context.
|
|
func RequestID(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
id := r.Header.Get("X-Request-Id")
|
|
if id == "" {
|
|
id = uuid.NewString()
|
|
}
|
|
ctx := context.WithValue(r.Context(), ctxKeyRequestID, id)
|
|
w.Header().Set("X-Request-Id", id)
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
})
|
|
}
|
|
|
|
func RequestIDFromCtx(ctx context.Context) string {
|
|
v, _ := ctx.Value(ctxKeyRequestID).(string)
|
|
return v
|
|
}
|
|
|
|
// Recover handles panics so a buggy handler can't take down the server.
|
|
func Recover(log *zap.Logger) func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
defer func() {
|
|
if rec := recover(); rec != nil {
|
|
log.Error("panic in handler",
|
|
zap.Any("panic", rec),
|
|
zap.String("path", r.URL.Path),
|
|
zap.ByteString("stack", debug.Stack()))
|
|
http.Error(w, `{"error":"internal server error"}`, http.StatusInternalServerError)
|
|
}
|
|
}()
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
}
|
|
|
|
// Logger logs one structured line per request.
|
|
func Logger(log *zap.Logger) func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
start := time.Now()
|
|
rw := &statusRecorder{ResponseWriter: w, status: 200}
|
|
next.ServeHTTP(rw, r)
|
|
log.Info("http",
|
|
zap.String("method", r.Method),
|
|
zap.String("path", r.URL.Path),
|
|
zap.Int("status", rw.status),
|
|
zap.Int64("duration_ms", time.Since(start).Milliseconds()),
|
|
zap.String("request_id", RequestIDFromCtx(r.Context())),
|
|
zap.String("ip", clientIP(r)))
|
|
})
|
|
}
|
|
}
|
|
|
|
// CORS returns a permissive CORS handler. The Analytics console calls the API
|
|
// directly from the browser during development.
|
|
func CORS(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
|
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Request-Id, X-Workspace-Id")
|
|
w.Header().Set("Access-Control-Max-Age", "86400")
|
|
if r.Method == http.MethodOptions {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
func clientIP(r *http.Request) string {
|
|
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
|
if i := strings.Index(xff, ","); i >= 0 {
|
|
return strings.TrimSpace(xff[:i])
|
|
}
|
|
return strings.TrimSpace(xff)
|
|
}
|
|
if rip := r.Header.Get("X-Real-Ip"); rip != "" {
|
|
return rip
|
|
}
|
|
return r.RemoteAddr
|
|
}
|
|
|
|
type statusRecorder struct {
|
|
http.ResponseWriter
|
|
status int
|
|
}
|
|
|
|
func (s *statusRecorder) WriteHeader(code int) {
|
|
s.status = code
|
|
s.ResponseWriter.WriteHeader(code)
|
|
}
|