Mọi service gRPC đều cần những việc chung cho mọi RPC: ghi log ai gọi gì, đo thời gian mỗi call để tìm endpoint chậm, kiểm tra xác thực, đếm metric, bắt panic. Cách ngây thơ là viết những việc đó trong từng handler — nhưng với hàng chục RPC, bạn lặp cùng một đoạn code hàng chục lần, và quên một chỗ là mất log/mất auth ở đó. Lời giải là interceptor: cơ chế middleware của gRPC, một lớp bọc quanh mọi RPC để làm việc chung một lần mà áp cho tất cả. Nó tương đương middleware của HTTP framework, nhưng cho gRPC. Bài này (phần 6 loạt gRPC nâng cao) chạy thật một chuỗi interceptor làm logging và auth.
Interceptor là gì: hàm bọc quanh handler
Một unary server interceptor là một hàm nhận handler thật và bọc quanh nó — nó chạy code trước, gọi handler, rồi chạy code sau:
// interceptor nhận handler thật, bọc quanh nó
func logInterceptor(ctx, req, info, handler) (any, error) {
t := time.Now()
resp, err := handler(ctx, req) // gọi handler THẬT
log(info.FullMethod, time.Since(t), status.Code(err))
return resp, err
}
func authInterceptor(ctx, req, info, handler) (any, error) {
if info.FullMethod == ".../Secure" {
md,_ := metadata.FromIncomingContext(ctx)
if md.Get("token") != valid {
return nil, status.Error(codes.Unauthenticated, ...) // chặn, handler KHÔNG chạy
}
}
return handler(ctx, req)
}
// CHAIN nhiều interceptor khi tạo server:
grpc.NewServer(grpc.ChainUnaryInterceptor(log, auth))
Điểm then chốt: interceptor có info.FullMethod nên biết đang bọc RPC nào, và có thể không gọi handler (như auth trả lỗi sớm → handler không bao giờ chạy). Nhiều interceptor ghép bằng ChainUnaryInterceptor, chạy theo thứ tự khai báo.

Hình 1: Interceptor là hàm nhận handler thật và bọc quanh — chạy code trước/sau, và có thể không gọi handler (auth trả lỗi sớm). info.FullMethod cho biết RPC nào. Ghép nhiều interceptor bằng ChainUnaryInterceptor(log, auth) — một chỗ duy nhất cho logging/metrics/auth/retry toàn hệ.
Đo thật: log mỗi call và auth chặn
Mình dựng server với chain hai interceptor (log + auth) trên go-lab, gọi vài RPC và xem log do interceptor tự sinh:

Hình 2: Kết quả thật. Interceptor log tự sinh cho mọi call mà không sửa handler: Fast 1µs OK, Slow 45.99ms OK (handler sleep 40ms). Gọi /Secure không token → auth interceptor chặn, log ghi Unauthenticated, client nhận lỗi, handler không chạy. Gọi /Secure có token đúng → OK, client nhận msg="secret".
Đọc kết quả:
- Log tự động cho mọi call:
Fastmất 1µs,Slowmất 45.99ms (handler sleep 40ms + overhead). Quan trọng: không một dòng code log nào nằm trong handlerFast/Slow/Secure— toàn bộ do interceptor. Thêm RPC mới, nó tự động được log và đo. Đây là nơi lý tưởng để đặt metric (nhưhttp_request_duration_secondscủa loạt Observability) cho gRPC. - Auth chặn trước khi handler chạy: gọi
/Securekhông token → auth interceptor thấy thiếu token, trảUnauthenticatedngay, handlerSecurekhông bao giờ chạy. Log vẫn ghi call đó (15µs, Unauthenticated) vì log interceptor bọc ngoài. Client nhận đúng mã lỗiUnauthenticated. - Qua được khi token đúng: gọi lại với
token=secret123trong metadata → auth cho qua, handler chạy, trảmsg="secret". Cùng một RPC, hai kết quả, khác biệt hoàn toàn do interceptor — handler không biết gì về auth.
Thông điệp cốt lõi: interceptor tách mối quan tâm xuyên suốt (cross-cutting concern) khỏi logic nghiệp vụ. Handler chỉ lo việc của nó (trả order, tính tiền); log, đo, auth, retry sống ở interceptor — viết một lần, áp cho tất cả, không lặp và không sót.
Thứ tự chain quan trọng
ChainUnaryInterceptor(log, auth) chạy log ngoài cùng, rồi auth, rồi handler — như các lớp vỏ hành. Thứ tự này có ý nghĩa: ở đây log bọc ngoài auth nên mọi call đều được log kể cả call bị auth chặn (ta thấy log "Secure 15µs Unauthenticated"). Nếu đảo lại (auth, log), call bị auth chặn sẽ không được log (vì auth trả lỗi trước khi tới log). Tuỳ mục đích: muốn log mọi call (kể cả bị từ chối) thì đặt log ngoài cùng; muốn chỉ log call hợp lệ thì đặt log trong. Thứ tự interceptor là một quyết định thiết kế, không tuỳ tiện.
Đánh đổi cần cân nhắc
Interceptor chạy cho MỌI call — một interceptor chậm làm chậm tất cả. Vì interceptor nằm trên đường đi của mọi RPC, bất kỳ việc nặng nào trong đó (gọi DB để check quyền mỗi call, ghi log đồng bộ ra đĩa) sẽ cộng độ trễ vào từng RPC. Đo thật cho thấy log interceptor chỉ tốn ~1µs — rẻ. Nhưng một auth interceptor query database mỗi call có thể thêm hàng chục ms cho mọi endpoint. Giữ interceptor nhẹ: cache kết quả auth, log bất đồng bộ, tránh I/O chặn trong đường nóng.
Streaming cần interceptor riêng. Demo dùng UnaryServerInterceptor — chỉ áp cho unary RPC. Streaming RPC (bài 3-5) cần StreamServerInterceptor, có chữ ký khác (bọc ServerStream thay vì req/resp đơn). Nếu service có cả unary lẫn streaming, phải đăng ký cả hai loại interceptor, và logic (như auth) thường phải viết hai lần hoặc dùng helper chung. Đây là điểm dễ sót: thêm auth cho unary mà quên streaming là lỗ hổng bảo mật.
Đừng nhồi quá nhiều vào interceptor. Interceptor tiện nên dễ bị lạm dụng — nhét cả logic nghiệp vụ, biến đổi dữ liệu, điều hướng phức tạp vào đó. Nhưng interceptor vô hình với người đọc handler: ai đọc handler Secure không thấy auth ở đâu cả, dễ nhầm. Giữ interceptor cho đúng cross-cutting concern thật (log, metric, auth, retry, recover panic); logic riêng của một RPC nên ở trong handler đó để rõ ràng.
Ba ý mang về
- Interceptor bọc mọi RPC, tách cross-cutting concern khỏi handler: đo thật log + đo thời gian tự sinh cho Fast (1µs) và Slow (46ms) mà không sửa handler nào — thêm RPC mới tự động được log/đo.
- Interceptor có thể chặn trước khi handler chạy: đo thật auth interceptor chặn /Secure thiếu token (trả Unauthenticated, handler không chạy) và cho qua khi token đúng — handler không cần biết gì về auth.
- Chain có thứ tự, giữ interceptor nhẹ và đúng vai: thứ tự chain quyết định call bị chặn có được log không; interceptor chạy mọi call nên phải nhẹ (tránh I/O chặn); streaming cần interceptor riêng; và chỉ đặt cross-cutting concern, không nhồi logic nghiệp vụ.
Nguồn
- gRPC Go — Interceptors: https://github.com/grpc/grpc-go/blob/master/examples/features/interceptor/README.md
- gRPC docs — Interceptors concept: https://grpc.io/docs/guides/interceptors/
- go-grpc-middleware — bộ interceptor thông dụng: https://github.com/grpc-ecosystem/go-grpc-middleware
Phần sau ta xử lý deadline và cancellation: cách gRPC lan truyền thời hạn qua context, và demo thật một call vượt deadline bị huỷ ngay thay vì treo vô hạn.