Backend Development 19 min read

Designing Unified Error Handling for gRPC and HTTP Services Using Protobuf

This article explains how to unify error handling between gRPC and traditional HTTP services by defining shared protobuf messages, mapping gRPC status codes to HTTP responses, and customizing response structures with additional fields like stat and msg for consistent business‑level error reporting.

Xueersi Online School Tech Team
Xueersi Online School Tech Team
Xueersi Online School Tech Team
Designing Unified Error Handling for gRPC and HTTP Services Using Protobuf

When a protobuf file defines an interface, using the same pb structure for both gRPC and existing HTTP services reveals differences in how errors are represented. gRPC typically returns an error value, while HTTP services rely on a stat field inside the response body.

Example protobuf message:

message GenerateTaskReply {
  string name = 1;
  uint32 age = 2;
}

gRPC return values

Success response:

{
  "name": "小明",
  "age": 12
}

Failure response:

nil, error:[stat:code.Unknown, msg:"失败了"]

HTTP return values

Typical HTTP body using the same protobuf structure:

{
  "name": "小明",
  "age": 12
}

When an error occurs, the HTTP body may contain a custom error object:

{
  "code": 500,
  "message": "service error",
  "reason": "ERROR_REASON"
}

In the existing system, the response format is:

{
  "stat": 1,
  "msg": "success",
  "data": {"name": "小明", "age": 12}
}
{
  "stat": 10000,
  "msg": "失败了",
  "data": {}
}
gRPC determines success or failure via the error return value, while HTTP determines it via the stat field in the body; this mismatch prevents sharing the same response structure.

The article then explores three research points:

gRPC error propagation

Meaning of gRPC status codes

How to design HTTP error responses

1. gRPC error propagation

Server code returning a standard error:

func (s *Server) SayHello(ctx context.Context, t *code.Request) (*code.Response, error) {
    return &code.Response{Msg: "Hello world"}, errors.New("query db err, id=1: record not found")
}

Client receives both response and error:

r, err := client.SayHello(ctx, &code.Request{})
if err != nil {
    fmt.Printf("%+v\n", err) // rpc error: code = Unknown desc = query db err, id=1: record not found
    grpcErr, _ := status.FromError(err)
    fmt.Printf("%d\n", grpcErr.Code()) // Unknown code = 2
    fmt.Printf("%+v\n", grpcErr.Message()) // query db err, id=1: record not found
} else {
    fmt.Printf("%+v\n", r)
}

gRPC provides status.Error(codes.Canceled, "msg") to create a structured error. The underlying Status struct contains Code , Message , and Details fields.

type Status struct {
    Code    int32  `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
    Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
    Details []*any.Any `protobuf:"bytes,3,rep,name=details,proto3" json:"details,omitempty"`
}

gRPC transmits errors over HTTP/2 using headers grpc-status , grpc-message , and optionally grpc-status-details-bin . The server encodes the Status into these headers, and the client decodes them back into a Status object.

// Server side header construction (simplified)
headerFields = append(headerFields, hpack.HeaderField{Name: "grpc-status", Value: strconv.Itoa(int(st.Code()))})
headerFields = append(headerFields, hpack.HeaderField{Name: "grpc-message", Value: encodeGrpcMessage(st.Message())})
if len(st.Details) > 0 {
    stBytes, _ := proto.Marshal(st.Proto())
    headerFields = append(headerFields, hpack.HeaderField{Name: "grpc-status-details-bin", Value: encodeBinHeader(stBytes)})
}
// Client side header parsing (simplified)
for _, hf := range frame.Fields {
    switch hf.Name {
    case "grpc-status":
        code, _ := strconv.ParseInt(hf.Value, 10, 32)
        rawStatusCode = codes.Code(uint32(code))
    case "grpc-message":
        grpcMessage = decodeGrpcMessage(hf.Value)
    case "grpc-status-details-bin":
        statusGen, _ = decodeGRPCStatusDetails(hf.Value)
    }
}
if statusGen == nil {
    statusGen = status.New(rawStatusCode, grpcMessage)
}

The grpc-status-details-bin header carries serialized Status details, which can include custom error information such as google.rpc.ErrorInfo with a metadata map.

2. gRPC status codes

gRPC defines a set of standard codes (e.g., Cancelled , Unknown , InvalidArgument ) that should be used instead of custom codes. These codes map to HTTP status codes, but the project prefers to keep its own stat field for business logic.

3. Designing HTTP responses

Kratos framework wraps gRPC Status into HTTP responses. A custom error type implements GRPCStatus() to provide the underlying Status . The server encodes errors into a unified JSON structure:

type HttpStandardResponse struct {
    Stat int32       `json:"stat"`   // business status: 1 success, 0 failure
    Code int32       `json:"code"`
    Msg  string      `json:"msg"`
    Data interface{} `json:"data"`
}

Encoder example:

func ErrorEncoderHandler(w http.ResponseWriter, r *http.Request, err error) {
    se := errors.FromError(err)
    statCode, _ := strconv.Atoi(se.Metadata["stat"])
    resp := &HttpStandardResponse{Stat: int32(statCode), Code: se.Code, Msg: se.Message, Data: struct{}{}}
    // marshal and write response
}

Decoder extracts stat and converts non‑success responses into errors.Error objects with metadata, preserving the original protobuf data field for successful calls.

func errorDecoder(ctx context.Context, res *http.Response, httpStandardResponse *HttpStandardResponse) error {
    if httpStandardResponse.Stat == codec.STAT_SUCCESS {
        return nil
    }
    md := map[string]string{"stat": strconv.Itoa(int(httpStandardResponse.Stat))}
    return errors.New(res.StatusCode, "", httpStandardResponse.Msg).WithMetadata(md)
}

Finally, the article shows how to generate a protoc plugin ( protoc-gen-go-errors ) that automatically adds stat and msg metadata to generated error constructors, enabling seamless error creation and checking across gRPC and HTTP boundaries.

By aligning error definitions, status code mappings, and response formats, the system can share a single protobuf definition while satisfying both gRPC and legacy HTTP error handling requirements.
backendgRPCProtobufHTTPerror handling
Xueersi Online School Tech Team
Written by

Xueersi Online School Tech Team

The Xueersi Online School Tech Team, dedicated to innovating and promoting internet education technology.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.