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 Recoverys.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:456. 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:789Color 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 firsts.Use(middleware.Recovery()) // Recovery afterPanics 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)}