66 "log/slog"
77 "net/http"
88 "net/http/httptest"
9+ "os"
910 "strconv"
1011 "sync/atomic"
1112 "testing"
@@ -133,7 +134,7 @@ func TestRateLimitContextOutcome(t *testing.T) {
133134 t .Run (tt .name , func (t * testing.T ) {
134135 t .Parallel ()
135136
136- got , ok := rateLimitContextOutcome (tt .err )
137+ got , ok := rateLimitContextOutcome (context . Background (), tt .err )
137138 if ok != tt .ok {
138139 t .Fatalf ("ok = %t, want %t" , ok , tt .ok )
139140 }
@@ -144,6 +145,63 @@ func TestRateLimitContextOutcome(t *testing.T) {
144145 }
145146}
146147
148+ func TestRateLimitDoesNotLogCanceledRequestWhenRedisReturnsSocketTimeout (t * testing.T ) {
149+ t .Parallel ()
150+
151+ var errorLogs atomic.Int64
152+ logger := slog .New (countingErrorHandler {count : & errorLogs })
153+ store := incrementFunc (func (context.Context , string , time.Duration ) (int64 , error ) {
154+ // os.ErrDeadlineExceeded is the public identity of the runtime's
155+ // poll.DeadlineExceededError, which formats as "i/o timeout".
156+ return 0 , os .ErrDeadlineExceeded
157+ })
158+ handler := RateLimit (store , config.RateLimit {Enabled : true , Requests : 2 , Window : time .Minute }, logger )(http .HandlerFunc (func (w http.ResponseWriter , _ * http.Request ) {
159+ w .WriteHeader (http .StatusNoContent )
160+ }))
161+
162+ ctx , cancel := context .WithCancel (t .Context ())
163+ cancel ()
164+ req , err := http .NewRequestWithContext (ctx , http .MethodPost , "http://example.test/mcp" , nil )
165+ if err != nil {
166+ t .Fatal (err )
167+ }
168+ req .RemoteAddr = "203.0.113.10:1234"
169+
170+ rec := httptest .NewRecorder ()
171+ handler .ServeHTTP (rec , req )
172+
173+ if got := errorLogs .Load (); got != 0 {
174+ t .Fatalf ("error logs = %d, want 0" , got )
175+ }
176+ }
177+
178+ func TestRateLimitLogsRedisSocketTimeoutAtWarnAndFailsClosed (t * testing.T ) {
179+ t .Parallel ()
180+
181+ var warningLogs atomic.Int64
182+ var errorLogs atomic.Int64
183+ logger := slog .New (recordingLogHandler {warnings : & warningLogs , errors : & errorLogs })
184+ store := incrementFunc (func (context.Context , string , time.Duration ) (int64 , error ) {
185+ return 0 , os .ErrDeadlineExceeded
186+ })
187+ handler := RateLimit (store , config.RateLimit {Enabled : true , Requests : 2 , Window : time .Minute }, logger )(http .HandlerFunc (func (w http.ResponseWriter , _ * http.Request ) {
188+ w .WriteHeader (http .StatusNoContent )
189+ }))
190+
191+ rec := httptest .NewRecorder ()
192+ handler .ServeHTTP (rec , requestWithIP (t , "203.0.113.10:1234" ))
193+
194+ if rec .Code != http .StatusServiceUnavailable {
195+ t .Fatalf ("status = %d, want %d" , rec .Code , http .StatusServiceUnavailable )
196+ }
197+ if got := warningLogs .Load (); got != 1 {
198+ t .Fatalf ("warning logs = %d, want 1" , got )
199+ }
200+ if got := errorLogs .Load (); got != 0 {
201+ t .Fatalf ("error logs = %d, want 0" , got )
202+ }
203+ }
204+
147205func TestClientIPPrefersFlyHeaderAndNormalizes (t * testing.T ) {
148206 t .Parallel ()
149207
@@ -205,3 +263,32 @@ func (h countingErrorHandler) WithAttrs([]slog.Attr) slog.Handler {
205263func (h countingErrorHandler ) WithGroup (string ) slog.Handler {
206264 return h
207265}
266+
267+ type recordingLogHandler struct {
268+ warnings * atomic.Int64
269+ errors * atomic.Int64
270+ }
271+
272+ var _ slog.Handler = recordingLogHandler {}
273+
274+ func (h recordingLogHandler ) Enabled (_ context.Context , level slog.Level ) bool {
275+ return level >= slog .LevelWarn
276+ }
277+
278+ func (h recordingLogHandler ) Handle (_ context.Context , record slog.Record ) error {
279+ switch {
280+ case record .Level >= slog .LevelError :
281+ h .errors .Add (1 )
282+ case record .Level >= slog .LevelWarn :
283+ h .warnings .Add (1 )
284+ }
285+ return nil
286+ }
287+
288+ func (h recordingLogHandler ) WithAttrs ([]slog.Attr ) slog.Handler {
289+ return h
290+ }
291+
292+ func (h recordingLogHandler ) WithGroup (string ) slog.Handler {
293+ return h
294+ }
0 commit comments