protoc-gen-sphere-errors
is a protoc plugin that generates error handling code from .proto
files. It is designed to inspect enum definitions within your protobuf files and automatically generate corresponding error handling code based on the sphere errors framework. This plugin creates Go code that provides structured error handling with HTTP status codes, error codes, and customizable messages.
This code is inspired by protoc-gen-go-errors but is specifically designed for the go-sphere framework.
- Generates error structs with HTTP status codes
- Supports custom error messages and reasons
- Provides
Join
andJoinWithMessage
methods for error composition - Integrates with the sphere error handling framework
- Supports default status codes for enum types
- Individual error value customization through options
To install protoc-gen-sphere-errors
, use the following command:
go install github.com/go-sphere/protoc-gen-sphere-errors@latest
You need to have the sphere errors proto definitions in your project. Add the following dependency to your buf.yaml
:
deps:
- buf.build/go-sphere/errors
To use protoc-gen-sphere-errors
with buf
, you can configure it in your buf.gen.yaml
file. Here is an example configuration:
version: v2
managed:
enabled: true
disable:
- file_option: go_package_prefix
module: buf.build/go-sphere/errors
override:
- file_option: go_package_prefix
value: github.com/go-sphere/sphere-layout/api
plugins:
- local: protoc-gen-sphere-errors
out: api
opt: paths=source_relative
Here's how to define error enums in your .proto
files:
syntax = "proto3";
package shared.v1;
import "sphere/errors/errors.proto";
enum TestError {
option (sphere.errors.default_status) = 500;
TEST_ERROR_UNSPECIFIED = 0;
TEST_ERROR_INVALID_FIELD_TEST1 = 1000 [(sphere.errors.options) = {
status: 400
reason: "INVALID_ARGUMENT"
message: "Invalid field_test1 value"
}];
TEST_ERROR_INVALID_PATH_TEST2 = 1001 [(sphere.errors.options) = {
status: 400
message: "Invalid path_test2 parameter"
}];
TEST_ERROR_UNAUTHORIZED = 1002 [(sphere.errors.options) = {
status: 401
reason: "UNAUTHORIZED"
message: "Authentication required"
}];
TEST_ERROR_FORBIDDEN = 1003 [(sphere.errors.options) = {
status: 403
reason: "FORBIDDEN"
message: "Permission denied"
}];
}
enum UserError {
option (sphere.errors.default_status) = 500;
USER_ERROR_UNSPECIFIED = 0;
USER_ERROR_NOT_FOUND = 2001 [(sphere.errors.options) = {
status: 404
message: "User not found"
}];
USER_ERROR_EMAIL_EXISTS = 2002 [(sphere.errors.options) = {
status: 409
message: "Email already exists"
}];
}
The plugin generates Go code with the following methods for each error enum:
Error() string
- Returns a string representation of the errorGetCode() int32
- Returns the error code (enum value)GetStatus() int32
- Returns the HTTP status codeGetMessage() string
- Returns the custom error messageGetReason() string
- Returns the error reason (if specified)Join(errs ...error) error
- Wraps the error with additional errorsJoinWithMessage(msg string, errs ...error) error
- Wraps with custom message
Example generated code for the TestError
enum:
// Error implements the error interface
func (e TestError) Error() string {
switch e {
case TestError_TEST_ERROR_UNSPECIFIED:
return "TestError:TEST_ERROR_UNSPECIFIED"
case TestError_TEST_ERROR_INVALID_FIELD_TEST1:
return "TestError:TEST_ERROR_INVALID_FIELD_TEST1"
case TestError_TEST_ERROR_INVALID_PATH_TEST2:
return "TestError:TEST_ERROR_INVALID_PATH_TEST2"
case TestError_TEST_ERROR_UNAUTHORIZED:
return "TestError:TEST_ERROR_UNAUTHORIZED"
case TestError_TEST_ERROR_FORBIDDEN:
return "TestError:TEST_ERROR_FORBIDDEN"
default:
return "TestError:UNKNOWN_ERROR"
}
}
// GetCode returns the error code (enum value)
func (e TestError) GetCode() int32 {
return int32(e)
}
// GetStatus returns the HTTP status code
func (e TestError) GetStatus() int32 {
switch e {
case TestError_TEST_ERROR_UNSPECIFIED:
return 500 // Uses default_status
case TestError_TEST_ERROR_INVALID_FIELD_TEST1:
return 400
case TestError_TEST_ERROR_INVALID_PATH_TEST2:
return 400
case TestError_TEST_ERROR_UNAUTHORIZED:
return 401
case TestError_TEST_ERROR_FORBIDDEN:
return 403
default:
return 500 // Uses default_status
}
}
// GetMessage returns the custom error message
func (e TestError) GetMessage() string {
switch e {
case TestError_TEST_ERROR_INVALID_FIELD_TEST1:
return "Invalid field_test1 value"
case TestError_TEST_ERROR_INVALID_PATH_TEST2:
return "Invalid path_test2 parameter"
case TestError_TEST_ERROR_UNAUTHORIZED:
return "Authentication required"
case TestError_TEST_ERROR_FORBIDDEN:
return "Permission denied"
default:
return ""
}
}
// GetReason returns the error reason
func (e TestError) GetReason() string {
switch e {
case TestError_TEST_ERROR_INVALID_FIELD_TEST1:
return "INVALID_ARGUMENT"
case TestError_TEST_ERROR_UNAUTHORIZED:
return "UNAUTHORIZED"
case TestError_TEST_ERROR_FORBIDDEN:
return "FORBIDDEN"
default:
return ""
}
}
// Join wraps the error with additional errors
func (e TestError) Join(errs ...error) error {
allErrs := append(errs, e)
msg := e.GetMessage()
if msg == "" {
msg = e.Error()
}
return statuserr.NewError(
e.GetStatus(),
e.GetCode(),
msg,
errors.Join(allErrs...),
)
}
// JoinWithMessage wraps the error with a custom message and additional errors
func (e TestError) JoinWithMessage(msg string, errs ...error) error {
allErrs := append(errs, e)
return statuserr.NewError(
e.GetStatus(),
e.GetCode(),
msg,
errors.Join(allErrs...),
)
}
func (s *service) ValidateField(field string) error {
if field == "" {
return sharedv1.TestError_TEST_ERROR_INVALID_FIELD_TEST1
}
return nil
}
func (s *service) RunTest(ctx context.Context, req *sharedv1.RunTestRequest) (*sharedv1.RunTestResponse, error) {
if req.FieldTest1 == "" {
return nil, sharedv1.TestError_TEST_ERROR_INVALID_FIELD_TEST1
}
if req.PathTest2 <= 0 {
return nil, sharedv1.TestError_TEST_ERROR_INVALID_PATH_TEST2
}
// Business logic here...
return &sharedv1.RunTestResponse{
FieldTest1: req.FieldTest1,
PathTest1: req.PathTest1,
}, nil
}
func (s *service) ProcessUser(userID int64) error {
user, err := s.userRepo.GetUser(userID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return sharedv1.UserError_USER_ERROR_NOT_FOUND
}
// Wrap with additional context
return sharedv1.TestError_TEST_ERROR_INVALID_FIELD_TEST1.Join(err)
}
// Process user...
return nil
}
func (s *service) CreateUser(email string) error {
if s.userExists(email) {
return sharedv1.UserError_USER_ERROR_EMAIL_EXISTS.JoinWithMessage(
fmt.Sprintf("User with email %s already exists", email),
)
}
// Create user...
return nil
}
func ErrorHandlingMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
if len(c.Errors) > 0 {
err := c.Errors.Last().Err
// Check if it's a sphere error with status info
if statusErr, ok := err.(interface {
GetStatus() int32
GetCode() int32
GetMessage() string
}); ok {
c.JSON(int(statusErr.GetStatus()), gin.H{
"error": gin.H{
"code": statusErr.GetCode(),
"message": statusErr.GetMessage(),
},
})
return
}
// Default error handling
c.JSON(500, gin.H{
"error": gin.H{
"code": -1,
"message": "Internal server error",
},
})
}
}
}
- HTTP Status Code Integration: Each error automatically provides the correct HTTP status code
- Custom Error Messages: Support for human-readable error messages in multiple languages
- Error Reasons: Machine-readable reason codes for programmatic error handling
- Error Composition:
Join
andJoinWithMessage
methods for error wrapping and context - Default Status Codes: Enum-level default status codes with per-value overrides
- Framework Integration: Seamless integration with sphere error handling framework
- Type Safety: Generated errors implement Go's error interface with additional methods
enum UserError {
option (sphere.errors.default_status) = 500;
USER_ERROR_UNSPECIFIED = 0;
USER_ERROR_NOT_FOUND = 1001; // Clear what the error is
USER_ERROR_INVALID_EMAIL = 1002; // Specific validation error
USER_ERROR_DUPLICATE_EMAIL = 1003; // Specific conflict error
}
// Authentication errors (1000-1099)
enum AuthError {
option (sphere.errors.default_status) = 401;
AUTH_ERROR_UNSPECIFIED = 0;
AUTH_ERROR_INVALID_TOKEN = 1001;
AUTH_ERROR_TOKEN_EXPIRED = 1002;
AUTH_ERROR_INSUFFICIENT_PERMISSIONS = 1003;
}
// User management errors (2000-2099)
enum UserError {
option (sphere.errors.default_status) = 400;
USER_ERROR_UNSPECIFIED = 0;
USER_ERROR_NOT_FOUND = 2001;
USER_ERROR_INVALID_INPUT = 2002;
}
enum ValidationError {
option (sphere.errors.default_status) = 400;
VALIDATION_ERROR_UNSPECIFIED = 0;
VALIDATION_ERROR_REQUIRED_FIELD = 1001 [(sphere.errors.options) = {
status: 400
reason: "REQUIRED_FIELD_MISSING"
message: "Required field is missing"
}];
VALIDATION_ERROR_INVALID_FORMAT = 1002 [(sphere.errors.options) = {
status: 400
reason: "INVALID_FORMAT"
message: "Field format is invalid"
}];
}
400
: Bad Request - Client input validation errors401
: Unauthorized - Authentication required403
: Forbidden - Permission denied404
: Not Found - Resource doesn't exist409
: Conflict - Resource conflict (e.g., duplicate email)422
: Unprocessable Entity - Semantic validation errors429
: Too Many Requests - Rate limiting500
: Internal Server Error - Server-side errors502
: Bad Gateway - External service errors503
: Service Unavailable - Service temporarily down
The error plugin works seamlessly with other sphere components:
- protoc-gen-sphere: HTTP handlers automatically handle sphere errors and return appropriate status codes
- sphere/server/ginx: Response wrapper functions understand sphere errors
- sphere/core/errors: Base error handling framework
- protovalidate: Validation errors can be wrapped with sphere errors for consistent error responses
sphere.errors.default_status
: Sets the default HTTP status code for all values in the enum
sphere.errors.options
: Configures individual error valuesstatus
: HTTP status code (overrides default)reason
: Custom reason string (optional)message
: Human-readable error message