Overview
The recovery system provides automatic panic recovery for HTTP requests with proper error responses and detailed logging.
Functions
RecoverFromPanic
func RecoverFromPanic(app core.App, c *core.RequestEvent)
Recovers from panics and returns a structured 500 error response.
PocketBase application for logging
c
*core.RequestEvent
required
Request event where panic occurred
Location: core/logging/error_handler.go:152
Behavior:
- Captures panic with
recover()
- Logs panic with trace ID and stack trace
- Excludes static file requests from panic logs
- Returns HTML error page for browsers
- Returns JSON error response for API requests
Example Usage:
app.OnServe().BindFunc(func(e *core.ServeEvent) error {
e.Router.BindFunc(func(c *core.RequestEvent) error {
defer func() {
logging.RecoverFromPanic(app, c)
}()
return c.Next()
})
return e.Next()
})
SetupRecovery
func SetupRecovery(app core.App, e *core.ServeEvent)
Configures global panic recovery middleware.
ServeEvent to bind recovery middleware
Location: core/logging/logging.go:227
Example:
app.OnServe().BindFunc(func(e *core.ServeEvent) error {
logging.SetupRecovery(app, e)
return e.Next()
})
Error Response Type
type ErrorResponse struct {
Status string `json:"status"`
Message string `json:"message"`
Type string `json:"type,omitempty"`
Operation string `json:"operation,omitempty"`
StatusCode int `json:"status_code"`
TraceID string `json:"trace_id"`
Timestamp string `json:"timestamp"`
}
Location: core/logging/error_handler.go:20
JSON Response (API Routes)
{
"status": "Internal Server Error",
"message": "A panic occurred while processing your request",
"type": "panic",
"operation": "request_handler",
"status_code": 500,
"trace_id": "7kj9m2n4p6q8r0s2t4",
"timestamp": "2024-03-04T12:00:00Z"
}
HTML Response (Browser Requests)
Returns a styled HTML error page with:
- Error status and message
- Error type and operation
- Trace ID for debugging
- Timestamp
Content Negotiation
The recovery system automatically detects the appropriate response format:
API Routes (/api/*):
- Always return JSON
- Regardless of Accept header or User-Agent
Browser Requests:
- Return HTML for browsers (Mozilla, Chrome, Safari, Firefox)
- Return HTML when
Accept: text/html header is present
- Return JSON for all other clients
Location: core/logging/error_handler.go:179
{
"time": "2024-03-04T12:00:00Z",
"level": "ERROR",
"msg": "Panic recovered",
"event": "panic",
"trace_id": "7kj9m2n4p6q8r0s2t4",
"error": "runtime error: invalid memory address or nil pointer dereference",
"path": "/api/users/123",
"method": "GET",
"stack": "goroutine 42 [running]:\nruntime/debug.Stack()\n\t/usr/local/go/src/runtime/debug/stack.go:24 +0x65\n..."
}
Complete Examples
Global Recovery Setup
package main
import (
"github.com/magooney-loon/pb-ext/core/logging"
"github.com/pocketbase/pocketbase/core"
)
func main() {
app := pocketbase.New()
app.OnServe().BindFunc(func(e *core.ServeEvent) error {
// Setup panic recovery for all routes
logging.SetupRecovery(app, e)
// Register routes
e.Router.GET("/api/users", listUsersHandler)
return e.Next()
})
app.Start()
}
Manual Recovery in Handler
func riskyHandler(e *core.RequestEvent) error {
defer func() {
if r := recover(); r != nil {
logging.RecoverFromPanic(e.App, e)
}
}()
// Risky operation that might panic
data := riskyOperation()
return e.JSON(200, data)
}
Custom Recovery with Cleanup
func handlerWithCleanup(e *core.RequestEvent) error {
resource := acquireResource()
defer func() {
// Cleanup even on panic
resource.Release()
// Recover from panic
if r := recover(); r != nil {
e.App.Logger().Error("Panic during resource processing",
"error", r,
"resource_id", resource.ID,
)
logging.RecoverFromPanic(e.App, e)
}
}()
// Process resource
result := processResource(resource)
return e.JSON(200, result)
}
Recovery with Alerting
func setupRecoveryWithAlerting(app core.App) {
app.OnServe().BindFunc(func(e *core.ServeEvent) error {
e.Router.BindFunc(func(c *core.RequestEvent) error {
defer func() {
if r := recover(); r != nil {
traceID := c.Request.Header.Get(logging.TraceIDHeader)
// Log panic
app.Logger().Error("Panic recovered",
"trace_id", traceID,
"error", r,
)
// Send alert (non-blocking)
go sendPanicAlert(traceID, r)
// Recover and send error response
logging.RecoverFromPanic(app, c)
}
}()
return c.Next()
})
return e.Next()
})
}
func sendPanicAlert(traceID string, panicValue interface{}) {
// Send to monitoring service
alerting.Send(alerting.Alert{
Severity: "critical",
Title: "Application Panic",
Message: fmt.Sprintf("Panic: %v", panicValue),
TraceID: traceID,
})
}
Testing Panic Recovery
func TestPanicRecovery(t *testing.T) {
app := pocketbase.New()
app.OnServe().BindFunc(func(e *core.ServeEvent) error {
logging.SetupRecovery(app, e)
e.Router.GET("/panic", func(c *core.RequestEvent) error {
panic("intentional panic for testing")
})
return e.Next()
})
// Start server in test mode
go app.Start()
defer app.ResetBootstrapState()
// Make request that triggers panic
resp, err := http.Get("http://localhost:8090/panic")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
// Should return 500, not crash
if resp.StatusCode != http.StatusInternalServerError {
t.Errorf("Expected 500, got %d", resp.StatusCode)
}
// Should include trace ID
traceID := resp.Header.Get("X-Trace-ID")
if traceID == "" {
t.Error("Missing trace ID in response")
}
}
Excluded Paths
Panic logs are suppressed for these paths to reduce noise:
/service-worker.js
/favicon.ico
/manifest.json
/robots.txt
- Files ending in:
.map, .ico, .webmanifest
Location: core/logging/error_handler.go:157
Stack Trace Capture
The recovery system captures full stack traces using runtime/debug.Stack():
stack := string(debug.Stack())
app.Logger().Error("Panic recovered",
"trace_id", traceID,
"error", r,
"stack", stack,
)
Location: core/logging/error_handler.go:164
Best Practices
- Global Setup: Use
SetupRecovery() once during app initialization
- Don’t Suppress: Let panics propagate to recovery middleware, don’t silently recover
- Log Context: Include trace IDs and request context in panic logs
- Resource Cleanup: Use deferred cleanup before panic recovery
- Monitor Panics: Set up alerting for production panics
- Fix Root Cause: Panics indicate bugs - fix them, don’t just recover
- Testing: Test panic scenarios to ensure graceful degradation
Common Panic Scenarios
Nil Pointer Dereference
func handlerWithNilCheck(e *core.RequestEvent) error {
user := getUserFromContext(e) // May return nil
// This would panic if user is nil
// return e.JSON(200, user.Profile)
// Safe approach
if user == nil {
return e.JSON(404, map[string]string{
"error": "user not found",
})
}
return e.JSON(200, user.Profile)
}
Index Out of Bounds
func handlerWithBoundsCheck(e *core.RequestEvent) error {
items := getItems()
// This would panic if items is empty
// firstItem := items[0]
// Safe approach
if len(items) == 0 {
return e.JSON(404, map[string]string{
"error": "no items found",
})
}
firstItem := items[0]
return e.JSON(200, firstItem)
}
Type Assertion
func handlerWithTypeCheck(e *core.RequestEvent) error {
value := getValueFromCache("key")
// This would panic if value is not a string
// str := value.(string)
// Safe approach
str, ok := value.(string)
if !ok {
return e.JSON(500, map[string]string{
"error": "invalid cache value type",
})
}
return e.JSON(200, map[string]string{"value": str})
}