Recovery Middleware
The Recovery middleware catches panics in your application, prevents server crashes, and gracefully passes errors to the centralized error handler.
Usage
Basic Usage
import (
"go-slim.dev/slim"
"go-slim.dev/slim/middleware"
)
func main() {
s := slim.New()
// Recovery should come after Logger
s.Use(middleware.Logger())
s.Use(middleware.Recovery())
s.GET("/panic", func(c slim.Context) error {
panic("something went wrong!")
})
s.Start(":8080")
}
Custom Configuration
s := slim.New()
s.Use(middleware.RecoveryWithConfig(middleware.RecoveryConfig{
StackSize: 4 << 10, // 4 KB
DisableStackAll: false,
DisablePrintStack: false,
}))
Configuration Options
RecoveryConfig
type RecoveryConfig struct {
// StackSize defines the size of the stack to be printed
// Optional. Default value 4KB
StackSize int
// DisableStackAll disables formatting stack traces of all other goroutines
// Optional. Default value false
DisableStackAll bool
// DisablePrintStack disables printing stack trace
// Optional. Default value false
DisablePrintStack bool
}
Examples
1. Basic Panic Recovery
s := slim.New()
s.Use(middleware.Logger())
s.Use(middleware.Recovery())
s.GET("/panic", func(c slim.Context) error {
panic("unexpected error!")
})
s.Start(":8080")
When accessing /panic:
- Recovery middleware catches the panic
- Prints colored stack trace to stderr
- Returns 500 Internal Server Error
- Server continues running
2. Disable Stack Printing
s.Use(middleware.RecoveryWithConfig(middleware.RecoveryConfig{
DisablePrintStack: true, // Don't print stack info
}))
Suitable for production environments to avoid leaking sensitive information.
3. Print Only Current Goroutine Stack
s.Use(middleware.RecoveryWithConfig(middleware.RecoveryConfig{
DisableStackAll: true, // Only print current goroutine stack
}))
Reduces log output and improves readability.
4. Custom Stack Size
s.Use(middleware.RecoveryWithConfig(middleware.RecoveryConfig{
StackSize: 8 << 10, // 8 KB for deeper call stacks
}))
5. Integration with Logger Middleware
s := slim.New()
s.ErrorHandler = func(c slim.Context, err error) error {
middleware.LogEnd(c, err)
// Custom error response
return c.JSON(500, map[string]string{
"error": "Internal Server Error",
})
}
// Logger should come before Recovery
s.Use(middleware.Logger())
s.Use(middleware.Recovery())
s.GET("/panic", func(c slim.Context) error {
panic("oops!")
})
Output example:
2024/11/08 10:30:15.123 "GET /panic HTTP/1.1" from 127.0.0.1:54321 - 500 35B in 1.2ms
panic: oops!
-> github.com/your/project/handler.PanicHandler
/path/to/handler.go:123
main.main
/path/to/main.go:45
6. Handle Specific Panic Types
s.Use(middleware.Recovery())
s.Use(func(c slim.Context, next slim.HandlerFunc) error {
defer func() {
if r := recover(); r != nil {
// Handle specific panic types
if err, ok := r.(error); ok {
c.Error(err)
return
}
// Re-throw other panics
panic(r)
}
}()
return next(c)
})
Stack Trace Format
Recovery middleware provides beautified stack trace output:
Colored Output (TTY)
panic: something went wrong!
-> go-slim.dev/slim/middleware.panicHandler
/path/to/middleware.go:123
go-slim.dev/slim.(*Context).Next
/path/to/context.go:456
main.main
/path/to/main.go:789
Color legend:
- Red arrow (->): Where the panic occurred
- Purple: Package name
- Red: Function name (panic location)
- Green: Function name (other locations)
- Cyan: File name
- Green: Line number
Plain Text Output (Non-TTY)
In non-TTY environments (e.g., log files), output does not include color codes.
How It Works
- Catch Panic: Uses
defer recover()to catch panics in the handler chain - Collect Stack: Calls
runtime.Stack()to get stack trace - Format Output: Beautifies stack information and outputs to log
- Set Response:
- If response not written, sets 500 status code
- If WebSocket upgrade, does not modify response
- Continue Running: Server continues processing other requests
Middleware Order
Important: Recovery should come after Logger to ensure panics are properly logged:
s.ErrorHandler = func(c slim.Context, err error) error {
middleware.LogEnd(c, err) // Log error
return err
}
s.Use(middleware.Logger()) // Logger first
s.Use(middleware.Recovery()) // Recovery after
Panics Not Recovered
Recovery middleware will not recover the following panics:
http.ErrAbortHandler
s.GET("/abort", func(c slim.Context) error {
panic(http.ErrAbortHandler) // Will not be recovered
})
http.ErrAbortHandler is a special panic used to abort responses. Recovery will re-throw it.
Best Practices
1. Always Use Recovery Middleware
s := slim.New()
// After all other middleware (except Logger)
s.Use(middleware.Logger())
s.Use(middleware.Recovery())
2. Production Environment Configuration
func newRecoveryMiddleware() slim.MiddlewareFunc {
if os.Getenv("ENV") == "production" {
return middleware.RecoveryWithConfig(middleware.RecoveryConfig{
DisablePrintStack: true, // Don't print stack in production
})
}
return middleware.Recovery() // Print full stack in development
}
s.Use(newRecoveryMiddleware())
3. Centralized Error Handling
s.ErrorHandler = func(c slim.Context, err error) error {
middleware.LogEnd(c, err)
// Differentiate error types
if he, ok := err.(*slim.HTTPError); ok {
return c.JSON(he.Code, he)
}
// Hide internal error details
return c.JSON(500, map[string]string{
"error": "Internal Server Error",
"id": c.Header(slim.HeaderXRequestID),
})
}
4. Log Panics to Monitoring Systems
type customLogEntry struct {
logger *slog.Logger
}
func (c *customLogEntry) Panic(v any, stack []byte) {
// Log to logging system
c.logger.Error("panic recovered",
slog.Any("panic", v),
slog.String("stack", string(stack)))
// Send to monitoring system (e.g., Sentry)
sentry.CaptureException(fmt.Errorf("panic: %v", v))
// Print to stderr
middleware.PrintPrettyStack(v, stack)
}
s.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
NewEntry: func(c slim.Context) middleware.LogEntry {
return &customLogEntry{
logger: slog.Default(),
}
},
}))
s.Use(middleware.Recovery())
5. Graceful Degradation
s.GET("/api/data", func(c slim.Context) error {
defer func() {
if r := recover(); r != nil {
// Log error
log.Printf("Error processing request: %v", r)
// Return cached data or default value
c.JSON(200, getCachedData())
}
}()
// Code that might panic
data := fetchDataFromExternalAPI()
return c.JSON(200, data)
})
Integration with Other Monitoring Tools
Integration with Sentry
import (
"github.com/getsentry/sentry-go"
"go-slim.dev/slim/middleware"
)
func init() {
sentry.Init(sentry.ClientOptions{
Dsn: os.Getenv("SENTRY_DSN"),
})
}
s.Use(func(c slim.Context, next slim.HandlerFunc) error {
defer func() {
if r := recover(); r != nil {
// Send to Sentry
sentry.CaptureException(fmt.Errorf("panic: %v", r))
// Re-throw to let Recovery middleware handle
panic(r)
}
}()
return next(c)
})
s.Use(middleware.Recovery())
Custom Panic Handler
func customRecovery() slim.MiddlewareFunc {
return func(c slim.Context, next slim.HandlerFunc) error {
defer func() {
if r := recover(); r != nil {
// Get stack
stack := make([]byte, 4<<10)
n := runtime.Stack(stack, false)
stack = stack[:n]
// Log
log.Printf("PANIC: %v\n%s", r, stack)
// Send alert
sendAlert(fmt.Sprintf("Panic: %v", r))
// Return error response
if !c.Response().Written() {
c.JSON(500, map[string]string{
"error": "Internal Server Error",
})
}
}
}()
return next(c)
}
}
s.Use(customRecovery())
Testing Recovery Middleware
func TestRecovery(t *testing.T) {
s := slim.New()
s.Use(middleware.Recovery())
s.GET("/panic", func(c slim.Context) error {
panic("test panic")
})
req := httptest.NewRequest("GET", "/panic", nil)
rec := httptest.NewRecorder()
s.ServeHTTP(rec, req)
// Verify response status code
if rec.Code != 500 {
t.Errorf("expected 500, got %d", rec.Code)
}
}
Default Configuration
DefaultRecoveryConfig = RecoveryConfig{
StackSize: 4 << 10, // 4 KB
DisableStackAll: false,
DisablePrintStack: false,
}
Performance Considerations
- Stack Collection: Collecting stack information has some overhead, but only triggers on panic
- Stack Size: Default 4KB is sufficient for most cases, can be increased for deep call stacks
- Log Output: Beautifying output requires parsing stack, but doesn't significantly impact performance
Security Considerations
-
Don't expose stack information in production:
s.Use(middleware.RecoveryWithConfig(middleware.RecoveryConfig{
DisablePrintStack: true, // Production
})) -
Avoid leaking sensitive information:
s.ErrorHandler = func(c slim.Context, err error) error {
middleware.LogEnd(c, err)
// Don't return error details directly
return c.JSON(500, map[string]string{
"error": "Internal Server Error",
})
}
Common Questions
Q: Can Recovery catch panics in async code?
A: No. Recovery can only catch panics in the current goroutine. Async code needs separate handling:
s.GET("/async", func(c slim.Context) error {
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("Panic in goroutine: %v", r)
}
}()
// Async code
panic("async panic")
}()
return c.String(200, "OK")
})
Q: Why should Recovery come after Logger?
A: This ensures panics are properly logged. If Recovery comes before Logger, Logger may not be able to log panic information.
Q: Can I customize panic responses?
A: Yes. Recovery only catches the panic and sets a 500 status code. The actual response content is determined by the error handler:
s.ErrorHandler = func(c slim.Context, err error) error {
return c.JSON(500, customErrorResponse)
}