18 Reading Metadata on a gRPC Server
gRPC has become one of the most popular service-to-service communication frameworks in the cloud-native era. Thanks to its high performance and support for many programming languages, gRPC is widely adopted across large-scale projects. One of gRPC’s powerful yet often underrated features is metadata. Metadata is a set of key-value pairs that can be sent alongside every request or response in an RPC, similar to HTTP headers but more tightly integrated at the gRPC protocol level.
In this article, we’ll take a thorough look at how to read metadata on the gRPC server side, using an implementation example in Go (Golang). We’ll also walk through hands-on examples and simulations, tables, and flow diagrams to make the concepts easier to grasp.
Getting to Know Metadata in gRPC
Before diving into the implementation, let’s first understand the concept of metadata in gRPC. Metadata lets us carry additional information such as authentication tokens, request IDs, custom context, and so on—information that isn’t rigidly tied to the main data protocol (the protobuf message).
On HTTP/2 (which is the primary transport for gRPC), metadata is typically translated into headers. In gRPC, however, both the client and the server have dedicated APIs for interacting with metadata.
In general, metadata consists of key-value pairs like the following:
| Key | Value | Description |
|---|---|---|
| authorization | Bearer xxxxx | Authorization token |
| client-id | 12345 | Unique client ID |
| custom-data | some-information | Custom data as needed |
Flow Diagram for Reading Metadata on a gRPC Server
Processing metadata on the gRPC server side can be illustrated as follows:
flowchart TD
A[Client] -- Kirim RPC + Metadata --> B[gRPC Server]
B -- Interceptor/Handler Baca Metadata --> C[Business Logic]
C -- Response + Metadata Opsional --> A
In the flow above, when the client sends a request, the metadata is sent along with it. The server can read that metadata either in a regular handler or through an interceptor for cross-cutting concerns such as authentication or logging.
Metadata Structure in Go
In Go, the main package for working with metadata is google.golang.org/grpc/metadata.
Here are its most important functions:
- md, ok := metadata.FromIncomingContext(ctx)
Retrieves the metadata sent by the client from the context. - metadata.Pairs(“key”, “value”)
Creates new metadata for outbound use.
Reading Metadata in the Server Handler
Let’s build a simple service that can read the metadata sent by the client.
Service Definition (Protobuf)
1syntax = "proto3";
2
3package hello;
4
5service Greeter {
6 rpc SayHello (HelloRequest) returns (HelloResponse);
7}
8
9message HelloRequest {
10 string name = 1;
11}
12
13message HelloResponse {
14 string message = 1;
15}Server Implementation in Go
The core of reading metadata can be done inside each individual RPC handler.
1import (
2 "context"
3 "fmt"
4 "google.golang.org/grpc"
5 "google.golang.org/grpc/metadata"
6 pb "path/to/your/proto"
7)
8
9type server struct {
10 pb.UnimplementedGreeterServer
11}
12
13func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
14 // Read metadata from the context
15 md, ok := metadata.FromIncomingContext(ctx)
16 if !ok {
17 fmt.Println("No metadata was sent")
18 } else {
19 // Print all received metadata
20 for k, v := range md {
21 fmt.Printf("Metadata %s: %v\n", k, v)
22 }
23 }
24
25 // Get a specific value
26 clientIds := md.Get("client-id")
27 if len(clientIds) > 0 {
28 fmt.Println("Client ID:", clientIds[0])
29 }
30
31 return &pb.HelloResponse{
32 Message: fmt.Sprintf("Hello, %s!", req.Name),
33 }, nil
34}In the example above, the SayHello handler reads all the metadata sent by the client along with its values.
Simulation: Running the Server and Sending Metadata
Let’s simulate a client that sends metadata.
Example Client
1import (
2 "context"
3 "log"
4 "google.golang.org/grpc"
5 "google.golang.org/grpc/metadata"
6 pb "path/to/your/proto"
7 "time"
8)
9
10func main() {
11 conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
12 if err != nil {
13 log.Fatalf("failed to connect: %v", err)
14 }
15 defer conn.Close()
16
17 c := pb.NewGreeterClient(conn)
18 md := metadata.New(map[string]string{
19 "client-id": "abc-123",
20 "custom-data": "example-info",
21 })
22 ctx := metadata.NewOutgoingContext(context.Background(), md)
23
24 resp, err := c.SayHello(ctx, &pb.HelloRequest{Name: "Budi"})
25 if err != nil {
26 log.Fatalf("Error during request: %v", err)
27 }
28 log.Printf("Server reply: %s", resp.Message)
29}Output on the Server
1Metadata client-id: [abc-123]
2Metadata custom-data: [example-info]
3Client ID: abc-123The server successfully captured the metadata sent by the client.
Using an Interceptor for Metadata
At larger scale, reading metadata is usually done in an interceptor—middleware that can run logic outside the business core, such as authentication or audit logging.
Example Unary Interceptor in Go
1func metadataInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
2 md, ok := metadata.FromIncomingContext(ctx)
3 if ok {
4 if tokens := md.Get("authorization"); len(tokens) > 0 {
5 fmt.Println("Auth token:", tokens[0])
6 }
7 }
8 // Continue to the main handler
9 return handler(ctx, req)
10}
11
12// When setting up the server
13grpc.NewServer(
14 grpc.UnaryInterceptor(metadataInterceptor),
15)When to use an interceptor:
- Validating session tokens across every gRPC method
- Audit logging of metadata
- Monitoring and tracing
Table: Comparing Approaches to Reading Metadata
| Approach | Advantage | Disadvantage | Use Case |
|---|---|---|---|
| In the Handler | Lives together with business logic | Redundant across multiple methods | Use cases specific to a single RPC |
| In the Interceptor | DRY, implemented once globally | No per-RPC customization | Authentication, logging for all RPCs |
| Combination | Flexible, choose as needed | May require coordination | Cross-cutting + per-RPC custom data parsing |
Best Practices
- Prefix custom metadata with a unique identifier to avoid collisions (e.g.,
x-myapp-userid). - Avoid metadata for large payloads—use it for small data like tokens, trace IDs, and so on.
- Case insensitive – Metadata keys are not case sensitive, but a lower-case convention is preferable.
- Use interceptors for cross-cutting concerns, and handlers for per-method needs.
Conclusion
Reading metadata on a gRPC server isn’t just about pulling key-value pairs from a context—it’s the foundation for building services that are secure, maintainable, and scalable. By understanding how metadata works and the best practices for using it, we can build applications that are more modular, easier to extend, and more secure.
Don’t hesitate to explore the metadata feature more deeply, for example to implement rate limiting, distributed tracing, or an integrated monitoring system within your microservices ecosystem!