Cách một API báo lỗi quyết định client xử lý được tốt tới đâu. Nếu server chỉ trả về một chuỗi "user not found", client buộc phải so chuỗi để biết chuyện gì xảy ra — và chuỗi đó đổi một chữ là code client vỡ. REST thường dùng HTTP status code cho việc này, nhưng tập mã HTTP khá hẹp và hay bị dùng lộn xộn. gRPC chuẩn hoá chặt chẽ hơn: một bộ status code rõ nghĩa để client rẽ nhánh đúng, cộng rich error details để gửi lỗi có cấu trúc (không chỉ một câu text). Bài này (phần 7/12) đo thật cả hai trong go-lab.
Status code chuẩn và rich error details
gRPC định nghĩa khoảng 16 status code chuẩn, mỗi cái một ý nghĩa rõ ràng và ánh xạ gần với HTTP:
| gRPC code | Nghĩa | ~HTTP |
|---|---|---|
InvalidArgument |
input sai định dạng/giá trị | 400 |
Unauthenticated |
chưa/không xác thực | 401 |
PermissionDenied |
không đủ quyền | 403 |
NotFound |
không tìm thấy đối tượng | 404 |
AlreadyExists |
trùng (tạo cái đã có) | 409 |
ResourceExhausted |
vượt quota / rate limit | 429 |
Unavailable |
dịch vụ tạm ngừng (nên retry) | 503 |
Internal |
lỗi nội bộ không lường | 500 |
Server trả lỗi bằng status.Error(code, msg). Nhưng mạnh hơn nữa là rich error details: gắn các object có cấu trúc (như BadRequest với danh sách field vi phạm) để client đọc bằng code, không phải regex chuỗi:
st := status.New(codes.InvalidArgument, "du lieu khong hop le")
br := &errdetails.BadRequest{FieldViolations: []...{{Field:"email", Description:"..."}}}
stD, _ := st.WithDetails(br)
return nil, stD.Err() // client đọc từng field-violation, không parse chuỗi

Hình 1: Bộ status code chuẩn của gRPC và ánh xạ HTTP tương ứng. Trả lỗi đơn giản bằng status.Errorf(code, msg); trả lỗi có cấu trúc bằng status.WithDetails gắn errdetails.BadRequest để client đọc từng field vi phạm type-safe.
Đo thật: status code và field violations
Mình dựng service Users với Get (trả NotFound/InvalidArgument theo tình huống) và Create (validate với rich error details):

Hình 2: Đo thật. (1) Status code: Get(1001)→OK, Get(9999)→NotFound, Get(-1)→InvalidArgument. (2) Rich details: Create với email xấu và age=15 trả về InvalidArgument kèm hai field-violation (email phải chứa @, age phải ≥18) để client đọc từng field.
Kết quả thật:
- ① Status code theo tình huống:
Get(1001)→OK;Get(9999)→NotFound("khong tim thay user id=9999");Get(-1)→InvalidArgument("id phai > 0"). Client đọcstatus.Code(err)để rẽ nhánh: gặpNotFoundthì hiện trang 404, gặpUnavailablethì retry (vì có thể tạm thời), gặpInvalidArgumentthì báo người dùng sửa input (retry vô ích). Code quyết định hành vi — không đoán từ chuỗi. - ② Rich error details:
Create(email="khong-co-a-cong", age=15)→InvalidArgument, msg="du lieu khong hop le", kèm hai field-violation có cấu trúc:email → email phai chua @vàage → age phai >= 18. Client lấy ra từng cái bằngst.Details()— type-safe. Nhờ vậy giao diện có thể tô đỏ đúng ô email và ô age trên form, thay vì cố regex một câu lỗi tiếng Anh dài.
Khác biệt thực tế rất lớn: với lỗi chuỗi thuần, client "biết" có lỗi nhưng không biết làm gì một cách đáng tin. Với status code + rich details, client xử lý lỗi như dữ liệu có cấu trúc — rẽ nhánh theo code, hiển thị chi tiết theo field.
Đánh đổi cần cân nhắc
Chọn đúng code quan trọng hơn là có nhiều code. Lạm dụng Internal cho mọi lỗi làm client mất khả năng phân biệt — không biết nên retry hay báo user hay bỏ. Ngược lại, dùng sai (trả NotFound cho một lỗi quyền) khiến client xử lý lệch. Quy ước nhóm: lỗi do client (InvalidArgument, NotFound, PermissionDenied) thì client sửa; lỗi tạm thời (Unavailable, ResourceExhausted) thì retry với backoff; Internal chỉ cho lỗi thật sự ngoài dự liệu.
Đừng rò rỉ chi tiết nhạy cảm trong message/details. Message lỗi và details đi thẳng tới client — đừng nhét câu SQL, stack trace, hay đường dẫn nội bộ vào đó (nối với bài bảo mật). Internal nên trả message chung chung ("lỗi nội bộ"), còn chi tiết thật thì log phía server. Rich details chỉ nên chứa thông tin client cần và được phép biết (field nào sai), không phải nội tình hệ thống.
Rich details thêm phụ thuộc và kích thước. errdetails đến từ package genproto riêng, và mỗi detail là một message Protobuf được đóng gói vào trailer — tốn thêm byte. Với lỗi đơn giản, chỉ status.Error(code, msg) là đủ. Dùng rich details cho các lỗi mà client thực sự cần xử lý theo cấu trúc (validation form, quota) — đừng gắn cho mọi lỗi.
Ba ý mang về
- gRPC có status code chuẩn để client rẽ nhánh đúng. Đo thật: Get id không tồn tại →
NotFound, id âm →InvalidArgument. Client đọcstatus.Code(err)để quyết định retry hay báo user hay hiện 404 — thay vì so chuỗi text mong manh. - Rich error details cho lỗi có cấu trúc. Đo thật: Create với input xấu trả
InvalidArgumentkèm danh sách field-violation (email, age) đọc được type-safe. Giao diện tô đỏ đúng ô sai thay vì regex câu lỗi. - Chọn đúng code và đừng rò rỉ. Nhóm lỗi: do-client thì client sửa, tạm-thời thì retry, Internal cho ngoài-dự-liệu; message/details đi tới client nên không chứa SQL/stack/đường dẫn nội bộ; và chỉ dùng rich details khi client thực sự cần xử lý theo cấu trúc.
Nguồn
- gRPC — Status codes and their use: https://grpc.io/docs/guides/status-codes/
- gRPC — Error handling: https://grpc.io/docs/guides/error/
- Google APIs — Error model & errdetails: https://cloud.google.com/apis/design/errors
Phần sau ta thêm xác thực: truyền token qua metadata của gRPC, per-RPC credentials, và dùng interceptor để kiểm auth tập trung cho mọi RPC.