Goa supports several types of interceptors to handle different scenarios. This guide explains the different types and when to use them.
When designing interceptors, there are three key dimensions to consider:
Server-side vs Client-side:
Payload vs Result access:
Read vs Write access:
Interceptors only need to reference the attributes they want to access by name - they don’t need to redefine the complete attribute definition or description. The method design must include these attributes in its payload and result types.
Use this when you need to inspect but not modify data. Perfect for monitoring, logging, and validation:
var Monitor = Interceptor("Monitor", func() {
Description("Collects metrics without modifying the data")
// Read request size from payload
ReadPayload(func() {
Attribute("size") // Type and description come from payload type
})
// Read response status from result
ReadResult(func() {
Attribute("status") // Type and description come from result type
})
})
The ReadPayload
and ReadResult
DSL functions declare read-only access to payload and result attributes:
ReadPayload
or ReadResult
blockUse this pattern when the interceptor needs to modify or add data:
var Enricher = Interceptor("Enricher", func() {
Description("Adds context information to requests and responses")
// Add request ID to payload
WritePayload(func() {
Attribute("requestID") // Must be defined in payload type
})
// Add timing to result
WriteResult(func() {
Attribute("processedAt") // Must be defined in result type
})
})
The WritePayload
and WriteResult
DSL functions declare write access:
When an interceptor needs both read and write access, combine the patterns:
var DataProcessor = Interceptor("DataProcessor", func() {
Description("Processes both requests and responses")
// Transform request data
ReadPayload(func() {
Attribute("rawData") // Input data from payload
Attribute("format") // Current format
})
WritePayload(func() {
Attribute("processed") // Transformed data
Attribute("newFormat") // New format
})
// Transform response data
ReadResult(func() {
Attribute("status") // Response status
Attribute("data") // Response data
})
WriteResult(func() {
Attribute("enriched") // Enriched response
Attribute("metadata") // Added metadata
})
})
Key points about combining access patterns:
Server interceptors execute on the service implementation side, running after the request has been decoded but before the service method is called. They’re perfect for implementing cross-cutting concerns like logging, metrics collection, request enrichment, and response transformation.
Here’s an example of a server-side caching interceptor that caches responses for GET requests:
var Cache = Interceptor("Cache", func() {
Description("Implements response caching for GET requests")
// We need to read the record ID to use as cache key
ReadPayload(func() {
Attribute("recordID") // UUID from payload type
})
// We'll add caching metadata to the response
WriteResult(func() {
Attribute("cachedAt") // String from result type
Attribute("ttl") // Int from result type
})
})
This server-side interceptor demonstrates:
The service design must include these attributes:
var _ = Service("catalog", func() {
// Apply caching to all methods in the service
ServerInterceptor(Cache)
Method("get", func() {
Payload(func() {
// Define attribute used by Cache interceptor
Attribute("recordID", UUID, "Record identifier for cache key")
})
Result(func() {
// Define attributes used by Cache interceptor
Attribute("cachedAt", String, "When the response was cached")
Attribute("ttl", Int, "Time-to-live in seconds")
// Other result fields...
})
HTTP(func() {
GET("/{recordID}")
Response(StatusOK)
})
})
})
Client interceptors execute on the client side before requests are sent to the server. They enable client-side behaviors like request enrichment, response processing, and client-side caching.
Here’s an example of a client-side interceptor that adds client context and tracks rate limits:
var ClientContext = Interceptor("ClientContext", func() {
Description("Enriches requests with client context and tracks rate limits")
// Add client context to outgoing requests
WritePayload(func() {
Attribute("clientVersion") // String from payload type
Attribute("clientID") // UUID from payload type
Attribute("region") // String from payload type
})
// Track rate limiting information from responses
ReadResult(func() {
Attribute("rateLimit") // From result type
Attribute("rateLimitRemaining") // From result type
Attribute("rateLimitReset") // From result type
})
})
This client-side interceptor illustrates:
WritePayload
ReadResult
The service must define these attributes:
var _ = Service("inventory", func() {
// Ensure all client calls include context information
ClientInterceptor(ClientContext)
Method("list", func() {
Payload(func() {
// Business logic attributes
Attribute("page", Int, "Page number")
Attribute("perPage", Int, "Items per page")
// Required by ClientContext interceptor
Attribute("clientVersion", String, "Version of the client library")
Attribute("clientID", UUID, "Unique identifier for this client instance")
Attribute("region", String, "Geographic region of the client")
})
Result(func() {
// Business logic attributes
Attribute("items", ArrayOf(Item))
// Required by ClientContext interceptor
Attribute("rateLimit", Int, "Current rate limit")
Attribute("rateLimitRemaining", Int, "Remaining requests in current window")
Attribute("rateLimitReset", Int, "When the rate limit window resets")
})
})
})
Streaming interceptors handle streaming methods where either the payload, result, or both are streams of messages. They use special streaming variants of the access patterns:
ReadStreamingPayload
/WriteStreamingPayload
: For client streamsReadStreamingResult
/WriteStreamingResult
: For server streamsHere’s an example showing different streaming interceptor patterns:
// SERVER-SIDE interceptor that WRITES to streaming RESULTS
var ServerProgressTracker = Interceptor("ServerProgressTracker", func() {
Description("Adds progress information to server stream responses")
WriteStreamingResult(func() {
Attribute("percentComplete") // Float32 from streaming result type
Attribute("itemsProcessed") // Int from streaming result type
})
})
// CLIENT-SIDE interceptor that WRITES to streaming PAYLOADS
var ClientMetadataEnricher = Interceptor("ClientMetadataEnricher", func() {
Description("Enriches outgoing client stream messages with metadata")
WriteStreamingPayload(func() {
Attribute("clientTimestamp") // From streaming payload type
Attribute("clientRegion") // From streaming payload type
})
})
The streaming interceptor DSL introduces special patterns:
ReadStreamingPayload
/WriteStreamingPayload
for client streamsReadStreamingResult
/WriteStreamingResult
for server streamsExample service using streaming interceptors:
var _ = Service("fileProcessor", func() {
// Server streaming example
Method("processFile", func() {
Description("Process a file with progress updates")
Payload(FileRequest) // Single request
StreamingResult(func() { // Multiple responses
// Business logic fields
Attribute("data", Bytes)
// Required by ServerProgressTracker
Attribute("percentComplete", Float32)
Attribute("itemsProcessed", Int)
})
ServerInterceptor(ServerProgressTracker)
})
// Client streaming example
Method("uploadFile", func() {
Description("Upload a file in chunks")
StreamingPayload(func() { // Multiple requests
// Business logic fields
Attribute("chunk", Bytes)
// Required by ClientMetadataEnricher
Attribute("clientTimestamp", Int)
Attribute("clientRegion", String)
})
Result(UploadResult) // Single response
ClientInterceptor(ClientMetadataEnricher)
})
})