1// Package middleware provides HTTP middleware for the adapters layer.
2package middleware
3
4import (
5 "net/http"
6 "time"
7
8 "github.com/bnema/zerowrap"
9)
10
11// ResponseWriter wraps http.ResponseWriter to capture status code and bytes written.
12type ResponseWriter struct {
13 http.ResponseWriter
14 statusCode int
15 bytes int
16}
17
18// NewResponseWriter creates a new wrapped response writer.
19func NewResponseWriter(w http.ResponseWriter) *ResponseWriter {
20 return &ResponseWriter{
21 ResponseWriter: w,
22 statusCode: http.StatusOK, // Default status code
23 }
24}
25
26// WriteHeader captures the status code.
27func (rw *ResponseWriter) WriteHeader(code int) {
28 rw.statusCode = code
29 rw.ResponseWriter.WriteHeader(code)
30}
31
32// Write captures bytes written.
33func (rw *ResponseWriter) Write(b []byte) (int, error) {
34 n, err := rw.ResponseWriter.Write(b)
35 rw.bytes += n
36 return n, err
37}
38
39// StatusCode returns the captured status code.
40func (rw *ResponseWriter) StatusCode() int {
41 return rw.statusCode
42}
43
44// BytesWritten returns the number of bytes written.
45func (rw *ResponseWriter) BytesWritten() int {
46 return rw.bytes
47}
48
49// RequestLogger is a middleware that logs HTTP requests using zerowrap.
50// It also attaches the logger to the request context for downstream handlers.
51func RequestLogger(log zerowrap.Logger) func(http.Handler) http.Handler {
52 return func(next http.Handler) http.Handler {
53 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
54 start := time.Now()
55
56 // Wrap the response writer to capture status and bytes
57 rw := NewResponseWriter(w)
58
59 // Get client IP (handle X-Forwarded-For and X-Real-IP headers)
60 clientIP := getClientIP(r)
61
62 // Attach the logger to the request context for downstream handlers
63 ctx := zerowrap.WithCtx(r.Context(), log)
64 r = r.WithContext(ctx)
65
66 // Call the next handler
67 next.ServeHTTP(rw, r)
68
69 // Calculate duration
70 duration := time.Since(start)
71
72 // Log the request
73 log.Info().
74 Str(zerowrap.FieldLayer, "adapter").
75 Str(zerowrap.FieldAdapter, "http").
76 Str(zerowrap.FieldMethod, r.Method).
77 Str(zerowrap.FieldPath, r.URL.Path).
78 Str("query", r.URL.RawQuery).
79 Str(zerowrap.FieldHost, r.Host).
80 Str("user_agent", r.UserAgent()).
81 Str("referer", r.Referer()).
82 Str(zerowrap.FieldClientIP, clientIP).
83 Int(zerowrap.FieldStatus, rw.StatusCode()).
84 Int("bytes", rw.BytesWritten()).
85 Dur(zerowrap.FieldDuration, duration).
86 Str("proto", r.Proto).
87 Msg("HTTP request")
88 })
89 }
90}
91
92// getClientIP extracts the client IP from various headers.
93func getClientIP(r *http.Request) string {
94 // Check X-Forwarded-For header (comma-separated list, first is original client)
95 if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
96 // Take the first IP (original client)
97 for idx := 0; idx < len(xff); idx++ {
98 if xff[idx] == ',' {
99 return xff[:idx]
100 }
101 }
102 return xff
103 }
104
105 // Check X-Real-IP header
106 if xri := r.Header.Get("X-Real-IP"); xri != "" {
107 return xri
108 }
109
110 // Check X-Forwarded header (less common)
111 if xf := r.Header.Get("X-Forwarded"); xf != "" {
112 return xf
113 }
114
115 // Fallback to RemoteAddr
116 return r.RemoteAddr
117}
118
119// PanicRecovery middleware recovers from panics and logs them.
120func PanicRecovery(log zerowrap.Logger) func(http.Handler) http.Handler {
121 return func(next http.Handler) http.Handler {
122 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
123 defer func() {
124 if err := recover(); err != nil {
125 log.Error().
126 Str(zerowrap.FieldLayer, "adapter").
127 Str(zerowrap.FieldAdapter, "http").
128 Interface("panic", err).
129 Str(zerowrap.FieldMethod, r.Method).
130 Str(zerowrap.FieldPath, r.URL.Path).
131 Str(zerowrap.FieldHost, r.Host).
132 Msg("panic recovered")
133
134 http.Error(w, "Internal Server Error", http.StatusInternalServerError)
135 }
136 }()
137
138 next.ServeHTTP(w, r)
139 })
140 }
141}
142
143// CORS middleware adds CORS headers.
144func CORS(next http.Handler) http.Handler {
145 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
146 w.Header().Set("Access-Control-Allow-Origin", "*")
147 w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
148 w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
149
150 // Handle preflight OPTIONS request
151 if r.Method == "OPTIONS" {
152 w.WriteHeader(http.StatusOK)
153 return
154 }
155
156 next.ServeHTTP(w, r)
157 })
158}
159
160// Chain combines multiple middleware functions.
161func Chain(middlewares ...func(http.Handler) http.Handler) func(http.Handler) http.Handler {
162 return func(final http.Handler) http.Handler {
163 for i := len(middlewares) - 1; i >= 0; i-- {
164 final = middlewares[i](final)
165 }
166 return final
167 }
168}