Error Handler Example
This example demonstrates the WithErrorHandler ServerOption feature of sebuf, which allows you to customize how errors are handled and returned to clients.
Overview
The ErrorHandler function is called whenever an error occurs during request processing. You can use it to:
- Log errors without modifying the response
- Add custom headers to error responses
- Set custom HTTP status codes for specific error types
- Return custom error response bodies (proto.Message)
- Write directly to the response for full control
Quick Start
# Generate code and build
make all
# Run the server
make run
Error Handler Examples
1. Logging Only
Just log errors without changing the response:
func LoggingErrorHandler(w http.ResponseWriter, r *http.Request, err error) proto.Message {
log.Printf("[%s] Error: %v", r.Header.Get("X-Request-ID"), err)
return nil // Use default response
}
Add tracking headers while keeping default response:
func HeaderErrorHandler(w http.ResponseWriter, r *http.Request, err error) proto.Message {
w.Header().Set("X-Error-ID", uuid.NewString())
w.Header().Set("X-Error-Timestamp", time.Now().UTC().Format(time.RFC3339))
return nil // Use default response with custom headers
}
3. Custom Status Codes
Override the default HTTP status codes:
func StatusCodeErrorHandler(w http.ResponseWriter, r *http.Request, err error) proto.Message {
var valErr *sebufhttp.ValidationError
if errors.As(err, &valErr) {
w.WriteHeader(http.StatusUnprocessableEntity) // 422 instead of 400
}
return nil // Framework marshals default error with custom status
}
4. Custom Response Body
Return a custom protobuf message as the error response:
func CustomBodyErrorHandler(w http.ResponseWriter, r *http.Request, err error) proto.Message {
return &models.CustomError{
Code: "ERR001",
Message: err.Error(),
RequestId: r.Header.Get("X-Request-ID"),
Timestamp: time.Now().Unix(),
}
}
5. Full Control
Write directly to the response for complete control:
func FullControlErrorHandler(w http.ResponseWriter, r *http.Request, err error) proto.Message {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(`{"error": "custom format"}`))
return nil // Framework won't write anything since we already called Write()
}
Error Types
The handler receives the error and can inspect it using errors.As():
// Check for validation errors (HTTP 400)
var valErr *sebufhttp.ValidationError
if errors.As(err, &valErr) {
// Handle validation error
for _, v := range valErr.Violations {
fmt.Printf("Field %s: %s\n", v.Field, v.Description)
}
}
// Check for handler errors (HTTP 500)
var handlerErr *sebufhttp.Error
if errors.As(err, &handlerErr) {
// Handle service error
fmt.Printf("Error: %s\n", handlerErr.Message)
}
// Check for custom error types
var notFound *ErrNotFound
if errors.As(err, ¬Found) {
// Handle not found error
}
Registration
Register your service with a custom error handler:
services.RegisterUserServiceServer(server,
services.WithMux(mux),
services.WithErrorHandler(myErrorHandler),
)
Test Commands
# Create a user (success)
curl -X POST http://localhost:8080/api/v1/users \
-H "Content-Type: application/json" \
-H "X-Request-ID: test-123" \
-d '{"name": "John Doe", "email": "john@example.com"}'
# Validation error (short name)
curl -X POST http://localhost:8080/api/v1/users \
-H "Content-Type: application/json" \
-d '{"name": "J", "email": "invalid-email"}'
# Not found error
curl -v http://localhost:8080/api/v1/users/non-existent-id
# With request ID header
curl -v http://localhost:8080/api/v1/users/non-existent-id \
-H "X-Request-ID: my-trace-id-456"
Default Behavior
When no ErrorHandler is configured, or when the handler returns nil:
- ValidationError: Returns HTTP 400 with the ValidationError protobuf message
- Other errors: Returns HTTP 500 with the Error protobuf message
The response format (JSON or protobuf) matches the request's Content-Type header.