or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

admin.mdadvanced.mdclient-server.mdcredentials-security.mderrors-status.mdhealth.mdindex.mdinterceptors.mdload-balancing.mdmetadata-context.mdname-resolution.mdobservability.mdreflection.mdstreaming.mdtesting.mdxds.md
tile.json

metadata-context.mddocs/

Metadata and Context

Metadata provides a way to send request and response headers and trailers in gRPC. Context carries deadlines, cancellation signals, and request-scoped values.

Metadata Package

Import: google.golang.org/grpc/metadata

MD Type

type MD map[string][]string

MD is a mapping from metadata keys to values. Keys are case-insensitive and stored in lowercase. Values can have multiple entries.

Creating Metadata

// New creates an MD from a given key-value map
func New(m map[string]string) MD

// Pairs creates an MD from a variable number of key-value pairs
// Keys and values must alternate
func Pairs(kv ...string) MD

// Example
md := metadata.New(map[string]string{
    "key1": "val1",
    "key2": "val2",
})

md := metadata.Pairs(
    "key1", "val1",
    "key2", "val2",
)

Manipulating Metadata

// Append returns a new MD with the appended values
func (md MD) Append(k string, vals ...string) MD

// Get returns the values for a given key
func (md MD) Get(k string) []string

// Set sets the values for a given key, overwriting existing values
func (md MD) Set(k string, vals ...string)

// Delete removes the values for a given key
func (md MD) Delete(k string)

// Len returns the number of key-value pairs
func (md MD) Len() int

// Copy returns a copy of the MD
func (md MD) Copy() MD

// Join joins multiple MDs into a new MD
func Join(mds ...MD) MD

Example:

md := metadata.Pairs("key1", "val1")
md = md.Append("key2", "val2", "val3")

vals := md.Get("key2") // ["val2", "val3"]

md.Set("key1", "newval")

md.Delete("key2")

Context Integration

Outgoing Context (Client Side)

// NewOutgoingContext creates a new context with outgoing metadata
func NewOutgoingContext(ctx context.Context, md MD) context.Context

// AppendToOutgoingContext returns a context with appended metadata
// More efficient than creating new MD and using NewOutgoingContext
func AppendToOutgoingContext(ctx context.Context, kv ...string) context.Context

// FromOutgoingContext retrieves outgoing metadata from context
func FromOutgoingContext(ctx context.Context) (MD, bool)

Client Example:

// Add metadata to outgoing context
md := metadata.Pairs(
    "authorization", "Bearer token123",
    "timestamp", time.Now().String(),
)
ctx := metadata.NewOutgoingContext(context.Background(), md)

// Or append to existing context
ctx = metadata.AppendToOutgoingContext(ctx,
    "request-id", "12345",
    "client-version", "1.0.0",
)

// Make RPC with metadata
response, err := client.SomeMethod(ctx, request)

Incoming Context (Server Side)

// NewIncomingContext creates a new context with incoming metadata
func NewIncomingContext(ctx context.Context, md MD) context.Context

// FromIncomingContext retrieves incoming metadata from context
func FromIncomingContext(ctx context.Context) (MD, bool)

Server Example:

func (s *server) SomeMethod(ctx context.Context, req *pb.Request) (*pb.Response, error) {
    // Read incoming metadata
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return nil, status.Error(codes.Internal, "no metadata")
    }

    // Extract specific values
    tokens := md.Get("authorization")
    if len(tokens) == 0 {
        return nil, status.Error(codes.Unauthenticated, "missing authorization")
    }

    // Process request
    // ...

    return &pb.Response{}, nil
}

Sending and Receiving Metadata in RPCs

Client Side

Unary RPC

// Send metadata in context
ctx := metadata.AppendToOutgoingContext(ctx, "key", "value")

// Capture response headers and trailers
var header, trailer metadata.MD
r, err := client.SomeMethod(ctx, &req,
    grpc.Header(&header),
    grpc.Trailer(&trailer))

// Access metadata
log.Println("Header:", header)
log.Println("Trailer:", trailer)

Streaming RPC

// Create stream with metadata
ctx := metadata.AppendToOutgoingContext(ctx, "key", "value")
stream, err := client.StreamingMethod(ctx)

// Get header (blocks until available)
header, err := stream.Header()

// Send messages
stream.Send(&req)

// Get trailer (after stream completes)
trailer := stream.Trailer()

Server Side

Setting Headers and Trailers

// In google.golang.org/grpc package

// SetHeader sets header metadata (can be called multiple times)
func SetHeader(ctx context.Context, md metadata.MD) error

// SendHeader sends header metadata (can only be called once)
func SendHeader(ctx context.Context, md metadata.MD) error

// SetTrailer sets trailer metadata
func SetTrailer(ctx context.Context, md metadata.MD) error

Unary RPC

func (s *server) SomeMethod(ctx context.Context, req *pb.Request) (*pb.Response, error) {
    // Set header (will be sent before first response)
    header := metadata.Pairs("header-key", "header-value")
    if err := grpc.SetHeader(ctx, header); err != nil {
        return nil, err
    }

    // Or send immediately
    if err := grpc.SendHeader(ctx, header); err != nil {
        return nil, err
    }

    // Set trailer (will be sent after response)
    trailer := metadata.Pairs("trailer-key", "trailer-value")
    grpc.SetTrailer(ctx, trailer)

    return &pb.Response{}, nil
}

Streaming RPC

func (s *server) StreamingMethod(stream pb.Service_StreamingMethodServer) error {
    // Set header
    header := metadata.Pairs("stream-id", "12345")
    if err := stream.SetHeader(header); err != nil {
        return err
    }

    // Or send header immediately
    if err := stream.SendHeader(header); err != nil {
        return err
    }

    // Process stream
    for {
        req, err := stream.Recv()
        if err == io.EOF {
            break
        }
        if err != nil {
            return err
        }

        // Send response
        if err := stream.Send(&pb.Response{}); err != nil {
            return err
        }
    }

    // Set trailer before returning
    trailer := metadata.Pairs("request-count", "100")
    stream.SetTrailer(trailer)

    return nil
}

Binary Metadata

Metadata keys ending in "-bin" are treated as binary data and automatically base64 encoded/decoded.

// Binary metadata
md := metadata.Pairs(
    "key-bin", string([]byte{0x00, 0x01, 0x02}),
)

// gRPC automatically handles base64 encoding/decoding

Metadata Best Practices

  1. Use lowercase keys: Metadata keys are case-insensitive but stored lowercase
  2. Binary suffix: Use "-bin" suffix for binary metadata values
  3. Multiple values: Metadata supports multiple values per key
  4. Reserved keys: Avoid "grpc-" prefix (reserved by gRPC)
  5. Size limits: Be aware of header size limits (default 8KB)

Peer Information

The peer package provides access to peer information (client/server address and credentials).

Import: google.golang.org/grpc/peer

type Peer struct {
    Addr      net.Addr        // Remote address
    LocalAddr net.Addr        // Local address (experimental)
    AuthInfo  credentials.AuthInfo // Auth information
}

Accessing Peer Information

import "google.golang.org/grpc/peer"

// Client side - get peer info from context
func (s *server) SomeMethod(ctx context.Context, req *pb.Request) (*pb.Response, error) {
    p, ok := peer.FromContext(ctx)
    if ok {
        log.Printf("Client address: %v", p.Addr)
        log.Printf("Auth info: %v", p.AuthInfo)
    }
    return &pb.Response{}, nil
}

// Client side - capture peer info with call option
var p peer.Peer
r, err := client.SomeMethod(ctx, &req, grpc.Peer(&p))
log.Printf("Server address: %v", p.Addr)

Peer Package Functions

// NewContext creates a new context with peer information
func NewContext(ctx context.Context, p *Peer) context.Context

// FromContext retrieves peer information from context
func FromContext(ctx context.Context) (*Peer, bool)

Context Utilities

Method Information

// Method returns the method string for the server context
// Returns format: "/service/method"
func Method(ctx context.Context) (string, bool)

// MethodFromServerStream returns method string from server stream
func MethodFromServerStream(stream ServerStream) (string, bool)

Example:

func (s *server) UnaryMethod(ctx context.Context, req *pb.Request) (*pb.Response, error) {
    method, ok := grpc.Method(ctx)
    if ok {
        log.Printf("Method called: %s", method)
        // method = "/mypackage.MyService/UnaryMethod"
    }
    return &pb.Response{}, nil
}

func (s *server) StreamMethod(stream pb.Service_StreamMethodServer) error {
    method, ok := grpc.MethodFromServerStream(stream)
    if ok {
        log.Printf("Stream method: %s", method)
    }
    return nil
}

Context Best Practices

  1. Always use context: Pass context to all RPC calls
  2. Set deadlines: Use context.WithTimeout or context.WithDeadline
  3. Cancellation: Use context.WithCancel for early termination
  4. Propagation: Context is automatically propagated through interceptors
  5. Values: Use context values sparingly for request-scoped data

Complete Example

Client

import (
    "context"
    "log"
    "time"

    "google.golang.org/grpc"
    "google.golang.org/grpc/metadata"
    "google.golang.org/grpc/peer"
)

func makeRequest(client pb.ServiceClient) {
    // Create context with timeout
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    // Add outgoing metadata
    ctx = metadata.AppendToOutgoingContext(ctx,
        "authorization", "Bearer token123",
        "request-id", "unique-id-456",
        "client-version", "1.0.0",
    )

    // Prepare to capture response metadata and peer info
    var header, trailer metadata.MD
    var p peer.Peer

    // Make unary call
    resp, err := client.UnaryMethod(ctx, &pb.Request{},
        grpc.Header(&header),
        grpc.Trailer(&trailer),
        grpc.Peer(&p),
    )
    if err != nil {
        log.Fatalf("RPC failed: %v", err)
    }

    // Access response metadata
    log.Printf("Response header: %v", header)
    log.Printf("Response trailer: %v", trailer)
    log.Printf("Server address: %v", p.Addr)
    log.Printf("Response: %v", resp)
}

func makeStreamingRequest(client pb.ServiceClient) {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    // Add metadata
    ctx = metadata.AppendToOutgoingContext(ctx,
        "stream-id", "stream-789",
    )

    // Create stream
    stream, err := client.StreamingMethod(ctx)
    if err != nil {
        log.Fatalf("Failed to create stream: %v", err)
    }

    // Get header (blocks until available)
    header, err := stream.Header()
    if err != nil {
        log.Fatalf("Failed to get header: %v", err)
    }
    log.Printf("Stream header: %v", header)

    // Send and receive
    for i := 0; i < 5; i++ {
        if err := stream.Send(&pb.Request{}); err != nil {
            log.Fatalf("Send failed: %v", err)
        }

        resp, err := stream.Recv()
        if err != nil {
            log.Fatalf("Recv failed: %v", err)
        }
        log.Printf("Received: %v", resp)
    }

    // Close send side
    if err := stream.CloseSend(); err != nil {
        log.Fatalf("CloseSend failed: %v", err)
    }

    // Get trailer
    trailer := stream.Trailer()
    log.Printf("Stream trailer: %v", trailer)
}

Server

import (
    "context"
    "io"
    "log"

    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/metadata"
    "google.golang.org/grpc/peer"
    "google.golang.org/grpc/status"
)

type server struct {
    pb.UnimplementedServiceServer
}

func (s *server) UnaryMethod(ctx context.Context, req *pb.Request) (*pb.Response, error) {
    // Read incoming metadata
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return nil, status.Error(codes.Internal, "no metadata received")
    }

    // Extract and validate auth token
    tokens := md.Get("authorization")
    if len(tokens) == 0 {
        return nil, status.Error(codes.Unauthenticated, "missing auth token")
    }
    log.Printf("Auth token: %s", tokens[0])

    // Get method and peer info
    method, _ := grpc.Method(ctx)
    p, _ := peer.FromContext(ctx)
    log.Printf("Method: %s, Client: %v", method, p.Addr)

    // Set response header
    header := metadata.Pairs(
        "server-version", "1.0.0",
        "response-time", time.Now().String(),
    )
    if err := grpc.SendHeader(ctx, header); err != nil {
        return nil, err
    }

    // Process request
    // ...

    // Set trailer
    trailer := metadata.Pairs(
        "server-timing", "total;dur=50",
    )
    grpc.SetTrailer(ctx, trailer)

    return &pb.Response{}, nil
}

func (s *server) StreamingMethod(stream pb.Service_StreamingMethodServer) error {
    ctx := stream.Context()

    // Read incoming metadata
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return status.Error(codes.Internal, "no metadata received")
    }
    log.Printf("Stream metadata: %v", md)

    // Get method and peer
    method, _ := grpc.MethodFromServerStream(stream)
    p, _ := peer.FromContext(ctx)
    log.Printf("Stream method: %s, Client: %v", method, p.Addr)

    // Send header
    header := metadata.Pairs("stream-started", "true")
    if err := stream.SendHeader(header); err != nil {
        return err
    }

    // Process stream
    messageCount := 0
    for {
        req, err := stream.Recv()
        if err == io.EOF {
            break
        }
        if err != nil {
            return err
        }
        messageCount++

        // Send response
        if err := stream.Send(&pb.Response{}); err != nil {
            return err
        }
    }

    // Set trailer
    trailer := metadata.Pairs(
        "message-count", fmt.Sprintf("%d", messageCount),
    )
    stream.SetTrailer(trailer)

    return nil
}

Metadata Interceptor Example

// Client interceptor to add metadata to all requests
func metadataUnaryInterceptor(ctx context.Context, method string, req, reply interface{},
    cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {

    // Add metadata to all outgoing requests
    ctx = metadata.AppendToOutgoingContext(ctx,
        "client-id", "my-client",
        "timestamp", time.Now().Format(time.RFC3339),
    )

    return invoker(ctx, method, req, reply, cc, opts...)
}

// Server interceptor to validate metadata
func metadataServerInterceptor(ctx context.Context, req interface{},
    info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {

    // Validate incoming metadata
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return nil, status.Error(codes.InvalidArgument, "missing metadata")
    }

    clientIDs := md.Get("client-id")
    if len(clientIDs) == 0 {
        return nil, status.Error(codes.InvalidArgument, "missing client-id")
    }

    // Continue with handler
    return handler(ctx, req)
}