Episode ini membahas metadata sebagai header RPC, interceptor client dan server untuk unary maupun streaming, serta lifecycle callbacks untuk request logging, tracing, dan konteks otorisasi yang bisa dipakai ulang di seluruh service.

Setiap RPC bisa membawa informasi tambahan di luar payload utama. Informasi itulah metadata — pasangan key-value yang dikirim sebagai header HTTP/2, seperti token autentikasi, trace ID, atau region asal request. Metadata sendiri bermanfaat, tapi kekuatan sebenarnya muncul saat digabungkan dengan interceptor.
Interceptor adalah middleware gRPC: kode yang berjalan sebelum setiap RPC diproses, tanpa harus ditulis ulang di setiap method. Episode 6 membahas metadata, interceptor client dan server untuk unary maupun streaming, serta lifecycle callbacks untuk logging, tracing, dan otorisasi.
Client mengirim metadata lewat context. Konvensi yang wajib diingat: key di dalam metadata harus lowercase, karena HTTP/2 tidak membedakan huruf besar-kecil pada header:
ctx := metadata.AppendToOutgoingContext(
ctx,
"authorization", "Bearer <token>",
"x-trace-id", traceID,
)
res, err := client.GetProduct(ctx, &pb.ProductId{Id: "p-001"})metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer ...") menempelkan dua pasang key-value ke context yang kemudian dikirim sebagai header.
Di sisi server, metadata dibaca dari context yang masuk:
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.InvalidArgument, "metadata kosong")
}
token := strings.TrimPrefix(md.Get("authorization")[0], "Bearer ")Perhatikan bahwa md.Get("authorization") mengembalikan slice, karena satu key bisa punya banyak nilai. Server juga bisa mengirim metadata balik lewat grpc.SendHeader.
Interceptor menghindari boilerplate yang sama di semua pemanggilan. Contoh client interceptor unary untuk menambahkan trace ID dan mengukur latensi:
func tracingUnaryClient(
ctx context.Context,
method string,
req, reply any,
cc *grpc.ClientConn,
invoker grpc.UnaryInvoker,
opts ...grpc.CallOption,
) error {
start := time.Now()
err := invoker(ctx, method, req, reply, cc, opts...)
log.Printf("method %s selesai dalam %s", method, time.Since(start))
return err
}Interceptor dipasang saat channel dibuat:
conn, _ := grpc.NewClient("localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithUnaryInterceptor(tracingUnaryClient),
)Fungsi grpc.WithUnaryInterceptor(tracingUnaryClient) mendaftarkan interceptor; untuk streaming pakai grpc.WithStreamInterceptor.
Server interceptor membungkus setiap handler. Ini tempat yang tepat untuk otorisasi, logging, atau recovery dari panic:
func authUnaryServer(
ctx context.Context,
req any,
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (any, error) {
md, _ := metadata.FromIncomingContext(ctx)
if len(md["authorization"]) == 0 {
return nil, status.Error(codes.Unauthenticated, "token diperlukan")
}
if err := validateToken(md["authorization"][0]); err != nil {
return nil, status.Error(codes.Unauthenticated, "token tidak valid")
}
return handler(ctx, req)
}Pola penting: validasi dulu, lalu panggil handler(ctx, req) untuk melanjutkan ke method. Dengan ini, satu interceptor melindungi seluruh service tanpa menyentuh satu pun handler.
Interceptor adalah tempat lifecycle callbacks hidup: titik masuk sebelum RPC, dan titik keluar setelah RPC selesai — mirip defer di Go. Gabungkan keduanya untuk observability menyeluruh:
func logUnaryServer(ctx context.Context, req any,
info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
start := time.Now()
res, err := handler(ctx, req)
st, _ := status.FromError(err)
log.Printf("method=%s code=%s durasi=%s",
info.FullMethod, st.Code(), time.Since(start))
return res, err
}Log satu baris per panggilan dengan method, status code, dan durasi — dasar dari observability yang akan dibangun penuh di episode 14. info.FullMethod berisi nama lengkap seperti catalog.v1.CatalogService/GetProduct.
Untuk streaming, lifecycle diakses lewat wrapper: interceptor StreamServerInterceptor membungkus stream.Recv dan stream.Send agar setiap message bisa dihitung atau diawasi.
Inti yang harus dibawa pulang:
WithUnaryInterceptor dan WithStreamInterceptor.handler membuat satu interceptor melindungi seluruh service.Di episode 7 selanjutnya kita membahas konfigurasi gRPC, environment variables, dan deployment lokal — menyusun konfigurasi environment-aware, menggunakan variabel lingkungan untuk port, TLS, service discovery, dan retry policy, lalu menjalankan server gRPC di dalam container dan docker-compose. Interceptor kalian akan pindah dari mesin lokal ke environment produksi.