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

Ảnh chụp đoạn mã nền tối minh hoạ xử lý lỗi gRPC status code chuẩn không phải chuỗi text tuỳ tiện, gRPC có bộ mã lỗi chuẩn hoá để client xử lý đúng rich error details cho lỗi có cấu trúc thay vì parse chuỗi. Status code chuẩn và ánh xạ 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. Trả lỗi status.Error và rich details lỗi đơn giản code cộng message return nil status.Errorf codes.NotFound khong thay user id phần trăm d id rich error gắn chi tiết có cấu trúc cho client đọc bằng code st bằng status.New codes.InvalidArgument du lieu khong hop le br bằng errdetails.BadRequest FieldViolations Field email stD bằng st.WithDetails br return nil stD.Err client doc tung field-violation khong parse chuoi

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):

Ảnh chụp bảng kết quả đo thật status code và rich error details output thật go-lab grpc-go v1.67.1 errdetails.BadRequest. Một status code chuẩn theo tình huống Get 1001 tồn tại OK a@example.com, Get 9999 không tồn tại NotFound khong tim thay user id 9999, Get -1 id không hợp lệ InvalidArgument id phai lớn hơn 0, client đọc status.Code err để rẽ nhánh xử lý NotFound thì hiện 404 Unavailable thì retry InvalidArgument thì báo người dùng sửa input không đoán từ chuỗi text. Hai rich error details client đọc có cấu trúc Create email khong-co-a-cong age 15 code InvalidArgument msg du lieu khong hop le chi tiết client đọc từng field không parse chuỗi field-violation email email phai chua @ field-violation age age phai lớn hơn bằng 18 server gắn errdetails.BadRequest với danh sách field vi phạm client lấy ra từng field cộng lý do một cách type-safe st.Details để ví dụ tô đỏ đúng ô nhập sai trên form thay vì cố regex một câu lỗi tiếng Anh

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 đọc status.Code(err) để rẽ nhánh: gặp NotFound thì hiện trang 404, gặp Unavailable thì retry (vì có thể tạm thời), gặp InvalidArgument thì 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ằng st.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ề

  1. 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 đọc status.Code(err) để quyết định retry hay báo user hay hiện 404 — thay vì so chuỗi text mong manh.
  2. Rich error details cho lỗi có cấu trúc. Đo thật: Create với input xấu trả InvalidArgument kè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.
  3. 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

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.