Context Propagation

Overview

Remote actors and clustered grains often need more than just the message payload. They also need the context the message was sent with:

  • Correlation IDs (for log stitching)

  • Authentication and authorization tokens

  • Tenant / request / locale information

  • Other per-request metadata

Inside a single process, this metadata usually lives in context.Context. As soon as a message crosses a process boundary (HTTP, gRPC, gateway, cluster hop, etc.), that data is lost unless we explicitly propagate it.

ContextPropagator exists to solve that problem in a controlled, pluggable way.

// ContextPropagator defines how Go context values travel across remoting and cluster
// boundaries by injecting them into outbound HTTP headers and extracting them on the
// receiving side (trace IDs, auth tokens, correlation IDs, and similar metadata).
//
// Implementations should be stateless, safe for concurrent use, favor stable header keys,
// and avoid leaking sensitive data unless explicitly required. Validate inputs to guard
// against header injection or oversized header sets. Go-Akt relies on a ContextPropagator
// so that context-derived metadata survives hops to remote actors or cluster peers and
// can be read safely during messages handling via ReceiveContext.Context() or GrainContext.Context().
//
// Error handling:
//   - Inject should fail only when headers cannot be written.
//   - Extract should return a derived context and report parse issues via the error,
//     letting callers choose log-and-continue vs fail-fast policies.
type ContextPropagator interface {
	// Inject writes context values into headers for an outgoing request.
	// Implementations should not mutate ctx and must be safe for concurrent use.
	Inject(ctx context.Context, headers nethttp.Header) error

	// Extract reads headers from an incoming request and returns a new context
	// containing any propagated values. The returned context should derive from
	// the provided ctx to preserve cancellations and deadlines.
	Extract(ctx context.Context, headers nethttp.Header) (context.Context, error)
}

Purpose

  • Preserving intent across hops Without a propagator, each remote hop starts with a “fresh” context: deadlines and cancellations are lost, logs lose correlation IDs, and downstream services can’t make auth decisions based on upstream user info. ContextPropagator gives GoAkt a framework hook to carry that metadata from inbound HTTP → actors, between cluster nodes, and back out over HTTP, so ReceiveContext.Context() and GrainContext.Context() see a single logical request context end-to-end.

  • Decoupling headers from logic Environments encode metadata differently (X-Request-ID vs X-Correlation-ID, W3C traceparent vs B3, different auth schemes). Baking these into GoAkt would lock in tracing/auth strategies and tangle protocol details with application code. Instead, GoAkt depends only on the abstract contract: Inject (“from context.Context to headers”) and Extract (“from headers to a derived context.Context”). Each deployment supplies its own ContextPropagator that matches its header conventions and infrastructure.

  • Safety & observability Propagation can leak secrets, inflate headers, or break traces if misused. Centralising it in ContextPropagator gives a single, auditable place to decide which context keys may leave the process, cap header sizes, validate IDs (logging or failing when invalid), and enforce consistent header naming. This is far safer and more observable than ad-hoc header handling scattered across handlers and actors.

Configuration

Defining a ContextPropagator is only the first step. For it to actually be used, it must be plugged into remoting in two places:

  1. When enabling remoting (via the remote config option)

  2. When sending remote messages (via remoting options)

Guidelines

When writing your own ContextPropagator:

  1. Stateless & concurrent-safe

    • No mutable fields that change per call

    • No caches that require locking

    • If you must cache, use safe concurrency primitives and keep it small

  2. Stable header keys

    • Choose header names once (e.g. X-Correlation-ID, traceparent) and keep them stable

    • Document them so other services can interoperate

  3. Minimal, explicit data set

    • Only propagate values you explicitly intend to share

    • Do not blindly dump all context values into headers

  4. Security considerations

    • Treat auth tokens and user identifiers as sensitive

    • Only propagate them to trusted peers

    • Prefer references (IDs) over raw secrets when possible

  5. Validation & limits

    • Validate header values before trusting them

    • Consider applying size limits:

      • Truncate overly long IDs

      • Ignore overly large headers

    • Guard against header injection (e.g. reject values with line breaks)

Integration

At a high level, GoAkt integrates ContextPropagator like this:

  • Inbound HTTP → Actor/Grain

    • An HTTP handler receives a request with headers

    • Extract is called to build a derived context.Context

    • That context is attached to the ReceiveContext or GrainContext

    • Your handlers access it via ReceiveContext.Context() / GrainContext.Context()

  • Actor/Grain → Outbound HTTP / cluster hop

    • Before sending a remote message or HTTP call, GoAkt calls Inject

    • The context from the actor or grain is encoded into headers

    • The remote side’s ContextPropagator.Extract restores it

The result is a consistent request-context story across the entire system: from the edge, through the actor system runtime, and across cluster peers.

Applicability

You should implement or configure a ContextPropagator if:

  • You rely heavily on correlation IDs in logs

  • You enforce auth/tenant rules even on internal calls

  • You’re integrating Go-Akt into an existing HTTP or gRPC ecosystem with specific header conventions

  • You’re operating a multi-tenant or security-sensitive cluster where context leakage is a concern

If none of the above apply, you can start with a very basic propagator (or a no-op one) and evolve it as your observability and security needs grow.

Last updated