Echo Recover Middleware
Introduction
When developing web applications with Go and the Echo framework, handling unexpected errors is crucial for maintaining a stable and reliable application. In Go, a panic occurs when your program encounters an unrecoverable error during execution. Unhandled panics can crash your entire application, which is particularly problematic in production environments.
The Echo Recover middleware provides a safety net for your application by catching panics during request processing, converting them into HTTP 500 Internal Server Error responses, and logging the error details. This allows your server to continue running even when unexpected errors occur.
How Recover Middleware Works
The Recover middleware works by utilizing Go's built-in recover
function within a deferred function call. When a panic occurs inside your request handler, the middleware catches it, logs the details, and returns an appropriate error response to the client.
Basic Implementation
Here's how the Echo Recover middleware is implemented internally (simplified):
func Recover() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
defer func() {
if r := recover(); r != nil {
// Log the error
// Return an HTTP 500 error
c.Error(echo.NewHTTPError(http.StatusInternalServerError, "Internal Server Error"))
}
}()
return next(c)
}
}
}
Adding Recover Middleware to Your Echo Application
Basic Usage
package main
import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
// Create a new Echo instance
e := echo.New()
// Register the Recover middleware
e.Use(middleware.Recover())
// Routes
e.GET("/", func(c echo.Context) error {
return c.String(200, "Hello, World!")
})
e.GET("/panic", func(c echo.Context) error {
// This will cause a panic
panic("Something went terribly wrong!")
return nil
})
// Start server
e.Start(":8080")
}
In this example, when you access /panic
, the handler will panic. Without the Recover middleware, the entire application would crash. With the middleware, the panic is caught, the error is logged, and a 500 Internal Server Error is returned to the client while the server continues to run.
Customizing the Recover Middleware
Echo allows you to customize how the Recover middleware behaves through configuration options.
Custom Configuration
// Custom Recover middleware configuration
e.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{
// Skip middleware for specific routes
Skipper: func(c echo.Context) bool {
return c.Path() == "/skip-recover"
},
// Use a custom log function
LogErrorFunc: func(c echo.Context, err error, stack []byte) error {
fmt.Printf("Error: %v\n", err)
fmt.Printf("Stack trace: %s\n", stack)
return err
},
// Disable stack tracing
DisableStackAll: false,
// Limit the number of stack trace lines
StackSize: 4 * 1024, // 4 KB
// Don't log recovery
DisablePrintStack: false,
}))
Configuration Options Explained
- Skipper: Function to determine which requests to skip this middleware for
- LogErrorFunc: Custom function to handle error logging
- DisableStackAll: When set to true, disables formatting stack traces of all other goroutines
- StackSize: Limits the size of the stack trace (in bytes)
- DisablePrintStack: When set to true, prevents printing stack traces to the logs
Real-World Example: API with Graceful Error Handling
Let's look at a more complex example of an API that uses the Recover middleware to handle panics gracefully:
package main
import (
"database/sql"
"fmt"
"log"
"net/http"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
_ "github.com/mattn/go-sqlite3"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
var db *sql.DB
func main() {
// Initialize database connection
var err error
db, err = sql.Open("sqlite3", ":memory:")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Create tables
_, err = db.Exec(`CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
)`)
if err != nil {
log.Fatal(err)
}
// Insert test data
_, err = db.Exec(`INSERT INTO users (name, email) VALUES (?, ?)`, "John Doe", "[email protected]")
if err != nil {
log.Fatal(err)
}
// Create Echo instance
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{
LogErrorFunc: func(c echo.Context, err error, stack []byte) error {
log.Printf("ERROR: %s\n", err)
log.Printf("STACK: %s\n", stack)
// You could also send the error to a monitoring service like Sentry here
return err
},
}))
// Routes
e.GET("/users/:id", getUserHandler)
e.POST("/users", createUserHandler)
e.GET("/dangerous", dangerousHandler)
// Start server
e.Start(":8080")
}
func getUserHandler(c echo.Context) error {
id := c.Param("id")
var user User
err := db.QueryRow("SELECT id, name, email FROM users WHERE id = ?", id).Scan(&user.ID, &user.Name, &user.Email)
if err != nil {
if err == sql.ErrNoRows {
return echo.NewHTTPError(http.StatusNotFound, "User not found")
}
return echo.NewHTTPError(http.StatusInternalServerError, "Database error")
}
return c.JSON(http.StatusOK, user)
}
func createUserHandler(c echo.Context) error {
user := new(User)
if err := c.Bind(user); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid request payload")
}
result, err := db.Exec("INSERT INTO users (name, email) VALUES (?, ?)", user.Name, user.Email)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Could not create user")
}
id, _ := result.LastInsertId()
user.ID = int(id)
return c.JSON(http.StatusCreated, user)
}
func dangerousHandler(c echo.Context) error {
// This handler will panic
var nullMap map[string]string = nil
nullMap["key"] = "value" // This will cause a panic: assignment to nil map
return c.String(http.StatusOK, "You'll never see this")
}
In this example:
- We set up a simple REST API with user management
- We include custom logging for panics via
RecoverWithConfig
- We have a deliberately dangerous handler to test panic recovery
- When a panic occurs in
dangerousHandler
, the middleware catches it and returns a 500 response
Benefits of Using Recover Middleware
- Improved Stability: Your application continues running even when unexpected errors occur
- Better User Experience: Users receive proper error responses instead of connection errors
- Enhanced Debugging: Detailed stack traces help identify the root cause of panics
- Centralized Error Handling: All panic recoveries are managed in one place
When to Use Recover Middleware
The Recover middleware should be used in virtually all Echo applications, especially in production environments. However, during development, you might want to consider allowing panics to be more visible to help with debugging.
Summary
The Echo Recover middleware is an essential component for building robust web applications with the Echo framework. It:
- Catches panics that occur during request processing
- Prevents your application from crashing
- Logs error details for debugging purposes
- Returns appropriate error responses to clients
- Can be customized to handle errors according to your needs
By implementing this middleware, you add a crucial safety net to your application, ensuring it can gracefully handle unexpected errors and continue serving users even when things go wrong.
Additional Resources
- Echo Framework Official Documentation
- Go's Defer, Panic, and Recover
- Best Practices for Error Handling in Go
Exercises
- Create a simple Echo application that uses the Recover middleware and has a route that deliberately causes a panic.
- Customize the Recover middleware to log errors to a file instead of standard output.
- Implement a custom
LogErrorFunc
that includes additional context about the request in the error log. - Create a middleware setup that skips the Recover middleware for a specific route or user agent.
- Extend the real-world example to include sending error notifications to an external service.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)