Documentation Index
Fetch the complete documentation index at: https://mintlify.com/magooney-loon/pb-ext/llms.txt
Use this file to discover all available pages before exploring further.
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})
}