protoc-gen-sphere-errors
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.
Features
- 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
Installation
To install protoc-gen-sphere-errors
, use the following command:
go install github.com/go-sphere/protoc-gen-sphere-errors@latest
Prerequisites
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
Usage with Buf
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
Proto Definition Example
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_PATH_TEST2 = 1001 [(sphere.errors.options) = {
status: 400
reason: "INVALID_PATH"
message: "Invalid path parameter"
}];
TEST_ERROR_MISSING_FIELD = 1002 [(sphere.errors.options) = {
status: 400
reason: "MISSING_FIELD"
message: "Required field is missing"
}];
}
enum UserError {
option (sphere.errors.default_status) = 500;
USER_ERROR_UNSPECIFIED = 0;
USER_ERROR_NOT_FOUND = 2001 [(sphere.errors.options) = {
status: 404
reason: "USER_NOT_FOUND"
message: "User not found"
}];
USER_ERROR_INVALID_EMAIL = 2002 [(sphere.errors.options) = {
status: 400
reason: "INVALID_EMAIL"
message: "Invalid email format"
}];
USER_ERROR_ALREADY_EXISTS = 2003 [(sphere.errors.options) = {
status: 409
reason: "USER_EXISTS"
message: "User already exists"
}];
}
Generated Code
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
Usage in Code
Direct Error Returns
func (s *service) ValidateField(field string) error {
if field == "" {
return sharedv1.TestError_TEST_ERROR_MISSING_FIELD
}
return nil
}
Error Handling in HTTP Handlers
func (s *service) RunTest(ctx context.Context, req *sharedv1.RunTestRequest) (*sharedv1.RunTestResponse, error) {
if req.FieldTest1 == "" {
return nil, sharedv1.TestError_TEST_ERROR_MISSING_FIELD.Join(
fmt.Errorf("field_test1 cannot be empty"))
}
// Business logic...
user, err := s.userRepo.GetByID(ctx, req.UserId)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, sharedv1.UserError_USER_ERROR_NOT_FOUND.Join(err)
}
return nil, fmt.Errorf("failed to get user: %w", err)
}
return &sharedv1.RunTestResponse{
FieldTest1: req.FieldTest1,
}, nil
}
Custom Messages
func (s *service) CreateUser(ctx context.Context, req *CreateUserRequest) (*User, error) {
if !isValidEmail(req.Email) {
return nil, sharedv1.UserError_USER_ERROR_INVALID_EMAIL.JoinWithMessage(
fmt.Sprintf("Email '%s' does not match required format", req.Email), nil)
}
// Check if user exists
existing, _ := s.userRepo.GetByEmail(ctx, req.Email)
if existing != nil {
return nil, sharedv1.UserError_USER_ERROR_ALREADY_EXISTS.JoinWithMessage(
fmt.Sprintf("User with email '%s' already exists", req.Email), nil)
}
// Create user...
}
HTTP Response Integration
When used with Sphere’s HTTP server utilities, these errors are automatically converted to structured JSON responses:
{
"status": 404,
"code": 2001,
"error": "USER_NOT_FOUND",
"message": "User not found"
}
Best Practices
- Use meaningful error codes: Choose enum values that clearly indicate the error type
- Set appropriate HTTP status codes: Use standard HTTP status codes (400, 401, 403, 404, 500, etc.)
- Provide clear messages: Write user-friendly error messages that can be displayed to end users
- Use reasons for programmatic handling: Include reason strings that client applications can use for conditional logic
- Group related errors: Keep related errors in the same enum for better organization
- Preserve context with Join: Always use
.Join()
to wrap underlying errors for better debugging
Integration Notes
- Sphere’s Gin layer maps these to structured JSON with correct HTTP status
- Pair with a global error parser if you need to merge validation/notfound/custom errors
- The generated errors integrate seamlessly with Sphere’s server utilities for consistent API responses