Bài về middleware HTTP cho ta một mẫu đẹp: xâu chuỗi các lớp cắt ngang (log, auth) quanh handler. gRPC cần đúng thứ đó — nhưng nó không dùng http.Handler, nó có cơ chế riêng gọi là interceptor. Interceptor cho phép chèn log, xác thực, đo metrics, phục hồi panic vào mọi RPC ở một chỗ, thay vì lặp trong từng handler. Bài này dựng một chain interceptor chạy được, đo thật thứ tự thực thi và short-circuit, và chỉ ra nó song song với middleware HTTP thế nào.

Interceptor là middleware của gRPC

Một unary server interceptor có chữ ký cố định: nhận ctx, req, thông tin RPC (info), và handler tiếp theo. Nó bọc quanh handler — chạy code trước, gọi handler, chạy code sau (đúng mô hình củ hành như middleware HTTP):

func logItc(ctx context.Context, req any, info *grpc.UnaryServerInfo,
	handler grpc.UnaryHandler) (any, error) {
	t0 := time.Now()
	fmt.Printf("[log] -> %s\n", info.FullMethod) // tên RPC, vd /svc.Echo/Noi
	res, err := handler(ctx, req)                // gọi tiếp -> handler hoặc itc kế
	fmt.Printf("[log] <- %s (%v)\n", info.FullMethod, time.Since(t0))
	return res, err
}

info.FullMethod cho biết RPC nào đang được gọi — nền cho log và metrics theo từng method. Gọi handler(ctx, req) là "đi tiếp"; giá trị trả về là response của handler.

Ảnh chụp đoạn mã Go nền tối minh hoạ gRPC interceptor middleware cho RPC log auth metrics một chỗ, interceptor bằng middleware của gRPC bọc quanh handler log cộng đo thời gian chạy trước và sau handler củ hành func logItc ctx context Context req any info con trỏ grpc UnaryServerInfo handler grpc UnaryHandler any error t0 bằng time Now fmt Printf log mũi tên phải phần trăm s info FullMethod tên RPC res err bằng handler ctx req gọi tiếp handler hoặc itc kế fmt Printf log mũi tên trái phần trăm s info FullMethod time Since t0 return res err, auth đọc token từ metadata short-circuit nếu thiếu func authItc ctx req info handler any error md bằng metadata FromIncomingContext ctx như HTTP header nếu len md Get token bằng 0 return nil status Error codes Unauthenticated cần token không gọi handler RPC bị chặn return handler ctx req, xâu chuỗi đăng ký một lần cho mọi RPC gs bằng grpc NewServer grpc ChainUnaryInterceptor logItc authItc log ngoài auth trong áp cho tất cả method không sửa từng handler stream có riêng grpc ChainStreamInterceptor, so với HTTP middleware cùng mô hình củ hành code trước handler bằng vào sau bằng ra metadata thay cho HTTP header mang token request-id short-circuit không gọi handler trả status lỗi luôn có 4 loại unary stream x server client interceptor

Hình 1: Interceptor bọc quanh handler như middleware HTTP. logItc chạy trước/sau handler; authItc đọc token từ metadata và short-circuit nếu thiếu. ChainUnaryInterceptor đăng ký một lần cho mọi RPC.

Auth: metadata và short-circuit

gRPC không có "HTTP header" ở tầng ứng dụng — nó dùng metadata (cũng đi trong header HTTP/2 bên dưới). Interceptor auth đọc token từ metadata và, nếu thiếu, trả lỗi mà không gọi handler — short-circuit y hệt middleware HTTP không gọi next:

func authItc(ctx context.Context, req any, info *grpc.UnaryServerInfo,
	handler grpc.UnaryHandler) (any, error) {
	md, _ := metadata.FromIncomingContext(ctx)
	if len(md.Get("token")) == 0 {
		return nil, status.Error(codes.Unauthenticated, "cần token") // KHÔNG gọi handler
	}
	return handler(ctx, req)
}

Đăng ký chuỗi interceptor một lần lúc tạo server, áp cho tất cả method:

gs := grpc.NewServer(grpc.ChainUnaryInterceptor(logItc, authItc)) // log ngoài, auth trong

Đo thật: củ hành và short-circuit

Chạy thật (Go 1.23, grpc 1.67) hai tình huống:

Ảnh chụp bảng kết quả đo thật nền tối interceptor chạy kiểu củ hành quanh RPC auth short-circuit chặn handler, protoc cộng go run Go 1.23 grpc 1.67 arm64 10 core, request có token log ngoài auth trong handler lõi log mũi tên phải svc Echo Noi log vào interceptor ngoài auth OK auth kiểm token qua log mũi tên trái svc Echo Noi true err nil log ra đo thời gian kết quả vọng chào err nil handler chạy trả kết quả, request thiếu token auth chặn handler không chạy log mũi tên phải svc Echo Noi auth từ chối thiếu token return status không gọi handler log mũi tên trái svc Echo Noi err Unauthenticated cần token err code Unauthenticated client nhận đúng mã gRPC, cơ chế ChainUnaryInterceptor logItc authItc log bọc ngoài auth mỗi interceptor gọi handler ctx req để đi tiếp hoặc return luôn để short-circuit như middleware HTTP không gọi next info FullMethod cho biết RPC nào log metrics theo method metadata từ context mang token tương đương HTTP header, cốt lõi interceptor middleware của gRPC bọc quanh mọi RPC củ hành code trước handler chạy vào sau chạy ra chain đăng ký 1 lần áp cho tất cả method short-circuit auth thiếu token trả Unauthenticated chặn handler 4 loại unary stream x server client chọn đúng loại

Hình 2: Request có token — log vào → auth OK → handler → log ra, kết quả vọng: chào. Request thiếu token — auth từ chối, handler không chạy, client nhận đúng mã gRPC Unauthenticated. Thứ tự củ hành và short-circuit hệt middleware HTTP.

  • Có token: [log] -> ... → [auth] OK → handler → [log] <-, kết quả vọng: chào.
  • Thiếu token: [log] -> ... → [auth] TỪ CHỐI → [log] <-, handler không chạy, client nhận mã Unauthenticated.

Đúng mô hình củ hành: logItc bọc ngoài authItc, code trước handler(ctx,req) chạy lúc vào, code sau chạy lúc ra. Khi authItc trả status.Error mà không gọi handler, toàn bộ chuỗi dừng — nhưng logItc vẫn chạy phần "ra" của nó (vì nó đã gọi authItc, chính auth mới không gọi handler lõi). Client nhận mã lỗi gRPC chuẩn Unauthenticated, không phải một lỗi mơ hồ.

Đánh đổi cần cân nhắc

Stream có interceptor riêng — đừng quên. gRPC có bốn loại interceptor: unary/stream × server/client. UnaryServerInterceptor không áp cho streaming RPC; bạn phải viết StreamServerInterceptor riêng (bọc grpc.ServerStream). Nếu chỉ đăng ký unary interceptor, các stream RPC của bạn không qua log/auth — một lỗ hổng dễ bỏ sót. Đăng ký cả ChainStreamInterceptor khi có streaming.

Thứ tự interceptor quan trọng như middleware. Recover (phục hồi panic) nên ngoài cùng để bắt panic từ mọi lớp trong; Auth trước handler nghiệp vụ; Log/metrics thường ngoài để đo cả thời gian. ChainUnaryInterceptor giữ thứ tự khai báo (đầu tiên = ngoài cùng), nhưng bạn phải suy nghĩ về thứ tự đó — đặt sai gây panic không bắt được hoặc metrics thiếu.

Dùng thư viện có sẵn cho việc phổ biến. Đừng viết lại từ đầu log, metrics, retry, tracing — hệ sinh thái go-grpc-middleware cung cấp bộ interceptor đã kiểm chứng (Prometheus, OpenTelemetry, zap, recovery, auth). Viết tay interceptor cho logic đặc thù của bạn, nhưng tận dụng thư viện cho các mối quan tâm chuẩn.

Ba ý mang về

  1. Interceptor là middleware của gRPC: chữ ký (ctx, req, info, handler) bọc quanh mọi RPC theo mô hình củ hành — code trước handler(ctx,req) chạy lúc vào, code sau lúc ra; info.FullMethod cho biết RPC nào để log/metrics theo method; ChainUnaryInterceptor đăng ký một lần cho tất cả method.
  2. Short-circuit và metadata song song với HTTP middleware: đo thật, auth thiếu token trả status.Error(Unauthenticated) mà không gọi handler (client nhận đúng mã gRPC), và metadata từ context mang token — tương đương header của HTTP middleware.
  3. Đừng quên interceptor cho stream và thứ tự chain: gRPC có 4 loại (unary/stream × server/client) — unary interceptor không áp cho stream, phải đăng ký StreamServerInterceptor riêng; thứ tự chain quan trọng (recover ngoài cùng), và tận dụng go-grpc-middleware cho việc chuẩn.

Phần sau ta đào sâu vào chính định dạng dữ liệu mà gRPC dùng: Phần sau mổ xẻ cách Protocol Buffers mã hóa — varint, wire type, field number — và vì sao nó nhỏ gọn đến vậy so với JSON, đo từng byte.