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.
50func RequestLogger(log zerowrap.Logger) func(http.Handler) http.Handler {
51 return func(next http.Handler) http.Handler {
52 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
53 start := time.Now()
54
55 // Wrap the response writer to capture status and bytes
56 rw := NewResponseWriter(w)
57
58 // Get client IP (handle X-Forwarded-For and X-Real-IP headers)
59 clientIP := getClientIP(r)
60
61 // Call the next handler
62 next.ServeHTTP(rw, r)
63
64 // Calculate duration
65 duration := time.Since(start)
66
67 // Log the request
68 log.Info().
69 Str(zerowrap.FieldLayer, "adapter").
70 Str(zerowrap.FieldAdapter, "http").
71 Str(zerowrap.FieldMethod, r.Method).
72 Str(zerowrap.FieldPath, r.URL.Path).
73 Str("query", r.URL.RawQuery).
74 Str(zerowrap.FieldHost, r.Host).
75 Str("user_agent", r.UserAgent()).
76 Str("referer", r.Referer()).
77 Str(zerowrap.FieldClientIP, clientIP).
78 Int(zerowrap.FieldStatus, rw.StatusCode()).
79 Int("bytes", rw.BytesWritten()).
80 Dur(zerowrap.FieldDuration, duration).
81 Str("proto", r.Proto).
82 Msg("HTTP request")
83 })
84 }
85}
86
87// getClientIP extracts the client IP from various headers.
88func getClientIP(r *http.Request) string {
89 // Check X-Forwarded-For header (comma-separated list, first is original client)
90 if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
91 // Take the first IP (original client)
92 for idx := 0; idx < len(xff); idx++ {
93 if xff[idx] == ',' {
94 return xff[:idx]
95 }
96 }
97 return xff
98 }
99
100 // Check X-Real-IP header
101 if xri := r.Header.Get("X-Real-IP"); xri != "" {
102 return xri
103 }
104
105 // Check X-Forwarded header (less common)
106 if xf := r.Header.Get("X-Forwarded"); xf != "" {
107 return xf
108 }
109
110 // Fallback to RemoteAddr
111 return r.RemoteAddr
112}
113
114// PanicRecovery middleware recovers from panics and logs them.
115func PanicRecovery(log zerowrap.Logger) func(http.Handler) http.Handler {
116 return func(next http.Handler) http.Handler {
117 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
118 defer func() {
119 if err := recover(); err != nil {
120 log.Error().
121 Str(zerowrap.FieldLayer, "adapter").
122 Str(zerowrap.FieldAdapter, "http").
123 Interface("panic", err).
124 Str(zerowrap.FieldMethod, r.Method).
125 Str(zerowrap.FieldPath, r.URL.Path).
126 Str(zerowrap.FieldHost, r.Host).
127 Msg("panic recovered")
128
129 http.Error(w, "Internal Server Error", http.StatusInternalServerError)
130 }
131 }()
132
133 next.ServeHTTP(w, r)
134 })
135 }
136}
137
138// CORS middleware adds CORS headers.
139func CORS(next http.Handler) http.Handler {
140 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
141 w.Header().Set("Access-Control-Allow-Origin", "*")
142 w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
143 w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
144
145 // Handle preflight OPTIONS request
146 if r.Method == "OPTIONS" {
147 w.WriteHeader(http.StatusOK)
148 return
149 }
150
151 next.ServeHTTP(w, r)
152 })
153}
154
155// Chain combines multiple middleware functions.
156func Chain(middlewares ...func(http.Handler) http.Handler) func(http.Handler) http.Handler {
157 return func(final http.Handler) http.Handler {
158 for i := len(middlewares) - 1; i >= 0; i-- {
159 final = middlewares[i](final)
160 }
161 return final
162 }
163}