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