From 7f12b0c87ad05c2e109df2a315352e86833189c1 Mon Sep 17 00:00:00 2001 From: Aleksey Shakhmatov Date: Sun, 22 Mar 2026 21:47:33 +0300 Subject: [PATCH] Add server Timeout middleware for context-based request deadlines Wraps http.TimeoutHandler to return 503 when handlers exceed the configured duration. Unlike http.Server.WriteTimeout, this allows handlers to complete gracefully via context cancellation. Co-Authored-By: Claude Opus 4.6 (1M context) --- server/middleware_timeout.go | 15 ++++++++++ server/middleware_timeout_test.go | 49 +++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 server/middleware_timeout.go create mode 100644 server/middleware_timeout_test.go diff --git a/server/middleware_timeout.go b/server/middleware_timeout.go new file mode 100644 index 0000000..a447660 --- /dev/null +++ b/server/middleware_timeout.go @@ -0,0 +1,15 @@ +package server + +import ( + "net/http" + "time" +) + +// Timeout returns a middleware that limits request processing time. +// If the handler does not complete within d, the client receives a +// 503 Service Unavailable response. It wraps http.TimeoutHandler. +func Timeout(d time.Duration) Middleware { + return func(next http.Handler) http.Handler { + return http.TimeoutHandler(next, d, "Service Unavailable\n") + } +} diff --git a/server/middleware_timeout_test.go b/server/middleware_timeout_test.go new file mode 100644 index 0000000..74145d6 --- /dev/null +++ b/server/middleware_timeout_test.go @@ -0,0 +1,49 @@ +package server_test + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" + + "git.codelab.vc/pkg/httpx/server" +) + +func TestTimeout(t *testing.T) { + t.Run("handler completes within timeout", func(t *testing.T) { + handler := server.Timeout(1 * time.Second)( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("ok")) + }), + ) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/", nil) + handler.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("got status %d, want %d", w.Code, http.StatusOK) + } + }) + + t.Run("handler exceeds timeout returns 503", func(t *testing.T) { + handler := server.Timeout(10 * time.Millisecond)( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + select { + case <-time.After(1 * time.Second): + case <-r.Context().Done(): + } + w.WriteHeader(http.StatusOK) + }), + ) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/", nil) + handler.ServeHTTP(w, req) + + if w.Code != http.StatusServiceUnavailable { + t.Fatalf("got status %d, want %d", w.Code, http.StatusServiceUnavailable) + } + }) +}