Skip to content

Commit 3379fdb

Browse files
authored
Merge pull request #10 from stacklok/add-recovery-package
Add recovery package with panic recovery middleware
2 parents 79f1983 + bdd0645 commit 3379fdb

3 files changed

Lines changed: 142 additions & 0 deletions

File tree

recovery/doc.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
// Package recovery provides panic recovery middleware for HTTP handlers.
5+
//
6+
// The middleware recovers from panics in HTTP handlers and returns a
7+
// 500 Internal Server Error response to the client. This prevents a single
8+
// panicking request from crashing the entire server.
9+
//
10+
// # Basic Usage
11+
//
12+
// mux := http.NewServeMux()
13+
// mux.HandleFunc("/", handler)
14+
// wrappedMux := recovery.Middleware(mux)
15+
// http.ListenAndServe(":8080", wrappedMux)
16+
//
17+
// # Stability
18+
//
19+
// This package is Beta stability. The API may have minor changes before
20+
// reaching stable status in v1.0.0.
21+
package recovery

recovery/recovery.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package recovery
5+
6+
import (
7+
"net/http"
8+
)
9+
10+
// Middleware is an HTTP middleware that recovers from panics.
11+
// When a panic occurs, it returns a 500 Internal Server Error response
12+
// to the client, preventing the panic from crashing the server.
13+
//
14+
// TODO(#7): Add configurable logging support once common logging is
15+
// established across ToolHive. Currently panics are silently recovered.
16+
func Middleware(next http.Handler) http.Handler {
17+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
18+
defer func() {
19+
if recover() != nil {
20+
// TODO(#7): Log panic value and stack trace
21+
// stack := debug.Stack()
22+
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
23+
}
24+
}()
25+
next.ServeHTTP(w, r)
26+
})
27+
}

recovery/recovery_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package recovery
5+
6+
import (
7+
"context"
8+
"net/http"
9+
"net/http/httptest"
10+
"testing"
11+
12+
"github.com/stretchr/testify/assert"
13+
)
14+
15+
func TestMiddleware_NoPanic(t *testing.T) {
16+
t.Parallel()
17+
18+
// Create a test handler that does not panic
19+
testHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
20+
w.WriteHeader(http.StatusOK)
21+
_, _ = w.Write([]byte("success"))
22+
})
23+
24+
// Wrap with recovery middleware
25+
wrappedHandler := Middleware(testHandler)
26+
27+
// Create test request
28+
req := httptest.NewRequest(http.MethodGet, "/test", nil)
29+
rec := httptest.NewRecorder()
30+
31+
// Execute request
32+
wrappedHandler.ServeHTTP(rec, req)
33+
34+
// Verify response
35+
assert.Equal(t, http.StatusOK, rec.Code)
36+
assert.Equal(t, "success", rec.Body.String())
37+
}
38+
39+
func TestMiddleware_RecoverFromPanic(t *testing.T) {
40+
t.Parallel()
41+
42+
// Create a test handler that panics
43+
testHandler := http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
44+
panic("test panic")
45+
})
46+
47+
// Wrap with recovery middleware
48+
wrappedHandler := Middleware(testHandler)
49+
50+
// Create test request
51+
req := httptest.NewRequest(http.MethodGet, "/test", nil)
52+
rec := httptest.NewRecorder()
53+
54+
// Execute request - should not panic
55+
wrappedHandler.ServeHTTP(rec, req)
56+
57+
// Verify 500 Internal Server Error response
58+
assert.Equal(t, http.StatusInternalServerError, rec.Code)
59+
assert.Contains(t, rec.Body.String(), "Internal Server Error")
60+
}
61+
62+
func TestMiddleware_PreservesRequestContext(t *testing.T) {
63+
t.Parallel()
64+
65+
type contextKey string
66+
const key contextKey = "test-key"
67+
const value = "test-value"
68+
69+
var receivedValue string
70+
71+
// Create a test handler that reads from context
72+
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
73+
if v := r.Context().Value(key); v != nil {
74+
receivedValue = v.(string)
75+
}
76+
w.WriteHeader(http.StatusOK)
77+
})
78+
79+
// Wrap with recovery middleware
80+
wrappedHandler := Middleware(testHandler)
81+
82+
// Create test request with context value
83+
req := httptest.NewRequest(http.MethodGet, "/test", nil)
84+
ctx := context.WithValue(req.Context(), key, value)
85+
req = req.WithContext(ctx)
86+
rec := httptest.NewRecorder()
87+
88+
// Execute request
89+
wrappedHandler.ServeHTTP(rec, req)
90+
91+
// Verify context was preserved
92+
assert.Equal(t, http.StatusOK, rec.Code)
93+
assert.Equal(t, value, receivedValue)
94+
}

0 commit comments

Comments
 (0)