Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions recovery/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
// SPDX-License-Identifier: Apache-2.0

// Package recovery provides panic recovery middleware for HTTP handlers.
//
// The middleware recovers from panics in HTTP handlers and returns a
// 500 Internal Server Error response to the client. This prevents a single
// panicking request from crashing the entire server.
//
// # Basic Usage
//
// mux := http.NewServeMux()
// mux.HandleFunc("/", handler)
// wrappedMux := recovery.Middleware(mux)
// http.ListenAndServe(":8080", wrappedMux)
//
// # Stability
//
// This package is Beta stability. The API may have minor changes before
// reaching stable status in v1.0.0.
package recovery
27 changes: 27 additions & 0 deletions recovery/recovery.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
// SPDX-License-Identifier: Apache-2.0

package recovery

import (
"net/http"
)

// Middleware is an HTTP middleware that recovers from panics.
// When a panic occurs, it returns a 500 Internal Server Error response
// to the client, preventing the panic from crashing the server.
//
// TODO(#7): Add configurable logging support once common logging is
// established across ToolHive. Currently panics are silently recovered.
func Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if recover() != nil {
// TODO(#7): Log panic value and stack trace
// stack := debug.Stack()
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
94 changes: 94 additions & 0 deletions recovery/recovery_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
// SPDX-License-Identifier: Apache-2.0

package recovery

import (
"context"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
)

func TestMiddleware_NoPanic(t *testing.T) {
t.Parallel()

// Create a test handler that does not panic
testHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("success"))
})

// Wrap with recovery middleware
wrappedHandler := Middleware(testHandler)

// Create test request
req := httptest.NewRequest(http.MethodGet, "/test", nil)
rec := httptest.NewRecorder()

// Execute request
wrappedHandler.ServeHTTP(rec, req)

// Verify response
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "success", rec.Body.String())
}

func TestMiddleware_RecoverFromPanic(t *testing.T) {
t.Parallel()

// Create a test handler that panics
testHandler := http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
panic("test panic")
})

// Wrap with recovery middleware
wrappedHandler := Middleware(testHandler)

// Create test request
req := httptest.NewRequest(http.MethodGet, "/test", nil)
rec := httptest.NewRecorder()

// Execute request - should not panic
wrappedHandler.ServeHTTP(rec, req)

// Verify 500 Internal Server Error response
assert.Equal(t, http.StatusInternalServerError, rec.Code)
assert.Contains(t, rec.Body.String(), "Internal Server Error")
}

func TestMiddleware_PreservesRequestContext(t *testing.T) {
t.Parallel()

type contextKey string
const key contextKey = "test-key"
const value = "test-value"

var receivedValue string

// Create a test handler that reads from context
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if v := r.Context().Value(key); v != nil {
receivedValue = v.(string)
}
w.WriteHeader(http.StatusOK)
})

// Wrap with recovery middleware
wrappedHandler := Middleware(testHandler)

// Create test request with context value
req := httptest.NewRequest(http.MethodGet, "/test", nil)
ctx := context.WithValue(req.Context(), key, value)
req = req.WithContext(ctx)
rec := httptest.NewRecorder()

// Execute request
wrappedHandler.ServeHTTP(rec, req)

// Verify context was preserved
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, value, receivedValue)
}
Loading