Documentation
¶
Overview ¶
Package middleware defines the two middleware contracts every Forge transport composes: unary and stream.
UnaryMiddleware wraps a UnaryHandler, one request producing one reply. StreamMiddleware wraps a StreamHandler, one complete stream lifecycle — not one message; per-message behavior comes from decorating the ServerStream the handler receives. There is no single combined middleware type.
Middleware attaches in three places, all at construction time: server-wide through each transport server's WithMiddleware option, per-service and per-method through the plan types protoc-gen-go-middleware generates, and client-side through each client's WithClientMiddleware option. ChainUnary and ChainStream compose without validation; ComposeUnary and ComposeStream validate and are what generated wrappers call.
The middleware implementations Forge ships live in the subpackages — recovery, logging, validate, ratelimit, timeout, metadata, retry, circuitbreaker, selector — and tracing in the separate contrib/otel module. See docs/agent/middleware.md for the usage contract and docs/design/generated-middleware.md for the rationale.
Example (ComposingByHand) ¶
Example_composingByHand mirrors "Composing by hand": only for building your own runtime; generated wrappers already do this.
package main
import (
"context"
"fmt"
"sync/atomic"
"github.com/sylphylabs/forge/middleware"
)
type tagKey struct{}
// Tagging mirrors "Writing unary middleware": the outer function body runs
// once, at wrapper construction; the returned handler runs per request.
func Tagging(value string) middleware.UnaryMiddleware {
return func(next middleware.UnaryHandler) middleware.UnaryHandler {
return func(ctx context.Context, req any) (any, error) {
ctx = context.WithValue(ctx, tagKey{}, value)
return next(ctx, req)
}
}
}
// countingStream mirrors "Writing stream middleware": per-message behavior
// comes from decorating ServerStream, not from a per-message hook.
type countingStream struct {
middleware.ServerStream
received *atomic.Int64
}
func (s *countingStream) RecvMsg(m any) error {
if err := s.ServerStream.RecvMsg(m); err != nil {
return err
}
s.received.Add(1)
return nil
}
func Counting(received *atomic.Int64) middleware.StreamMiddleware {
return func(next middleware.StreamHandler) middleware.StreamHandler {
return func(request any, stream middleware.ServerStream) error {
return next(request, &countingStream{ServerStream: stream, received: received})
}
}
}
func main() {
a := Tagging("a")
b := Tagging("b")
c := Tagging("c")
next := func(_ context.Context, req any) (any, error) { return req, nil }
chained := middleware.ChainUnary(a, b, c) // no validation
handler, err := middleware.ComposeUnary(next, a, b) // validates, returns error
if err != nil {
fmt.Println(err)
return
}
_ = chained(next)
_ = handler
// ChainStream and ComposeStream are the stream equivalents.
_ = middleware.ChainStream(Counting(new(atomic.Int64)))
streamHandler, err := middleware.ComposeStream(
func(_ any, _ middleware.ServerStream) error { return nil },
Counting(new(atomic.Int64)),
)
if err != nil {
fmt.Println(err)
return
}
_ = streamHandler
fmt.Println("composed")
}
Output: composed
Example (StreamMiddleware) ¶
package main
import (
"context"
"fmt"
"sync/atomic"
"github.com/sylphylabs/forge/middleware"
)
// countingStream mirrors "Writing stream middleware": per-message behavior
// comes from decorating ServerStream, not from a per-message hook.
type countingStream struct {
middleware.ServerStream
received *atomic.Int64
}
func (s *countingStream) RecvMsg(m any) error {
if err := s.ServerStream.RecvMsg(m); err != nil {
return err
}
s.received.Add(1)
return nil
}
func Counting(received *atomic.Int64) middleware.StreamMiddleware {
return func(next middleware.StreamHandler) middleware.StreamHandler {
return func(request any, stream middleware.ServerStream) error {
return next(request, &countingStream{ServerStream: stream, received: received})
}
}
}
type nopStream struct{}
func (nopStream) Context() context.Context { return context.Background() }
func (nopStream) SendMsg(any) error { return nil }
func (nopStream) RecvMsg(any) error { return nil }
func main() {
var received atomic.Int64
handler := func(_ any, stream middleware.ServerStream) error {
var msg any
return stream.RecvMsg(&msg)
}
wrapped := Counting(&received)(handler)
if err := wrapped(nil, nopStream{}); err != nil {
fmt.Println(err)
}
fmt.Println(received.Load())
}
Output: 1
Example (UnaryMiddleware) ¶
package main
import (
"context"
"fmt"
"github.com/sylphylabs/forge/middleware"
"github.com/sylphylabs/forge/transport"
)
type tagKey struct{}
// Tagging mirrors "Writing unary middleware": the outer function body runs
// once, at wrapper construction; the returned handler runs per request.
func Tagging(value string) middleware.UnaryMiddleware {
return func(next middleware.UnaryHandler) middleware.UnaryHandler {
return func(ctx context.Context, req any) (any, error) {
ctx = context.WithValue(ctx, tagKey{}, value)
return next(ctx, req)
}
}
}
// callInfo mirrors the guide's transport snippet: call information comes from
// the transport context, and Operation is opaque.
func callInfo(ctx context.Context) {
if tr, ok := transport.FromServerContext(ctx); ok {
_ = tr.Operation()
_ = tr.Kind()
_ = tr.RequestHeader()
}
}
func main() {
handler := func(ctx context.Context, _ any) (any, error) {
callInfo(ctx)
return ctx.Value(tagKey{}), nil
}
wrapped := Tagging("tagged")(handler)
reply, err := wrapped(context.Background(), "request")
fmt.Println(reply, err)
}
Output: tagged <nil>
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type ServerStream ¶
ServerStream is the transport-neutral server stream surface available to middleware. Transport-specific capabilities remain on their native stream.
type StreamHandler ¶
type StreamHandler func(request any, stream ServerStream) error
StreamHandler handles one complete server stream lifecycle. Request is the decoded initial request for server-streaming methods and nil for client and bidirectional streaming methods.
func ComposeStream ¶
func ComposeStream(next StreamHandler, m ...StreamMiddleware) (StreamHandler, error)
ComposeStream validates and composes a stream handler during wrapper construction. It is intended for generated registration-time wiring.
type StreamMiddleware ¶
type StreamMiddleware func(StreamHandler) StreamHandler
StreamMiddleware wraps a complete server stream lifecycle.
func ChainStream ¶
func ChainStream(m ...StreamMiddleware) StreamMiddleware
ChainStream composes stream middleware in declaration order. The first middleware is the outermost wrapper and runs first on entry.
type UnaryHandler ¶
UnaryHandler handles one request and returns one reply.
func ComposeUnary ¶
func ComposeUnary(next UnaryHandler, m ...UnaryMiddleware) (UnaryHandler, error)
ComposeUnary validates and composes a unary handler during wrapper construction. It is intended for generated registration-time wiring.
type UnaryMiddleware ¶
type UnaryMiddleware func(UnaryHandler) UnaryHandler
UnaryMiddleware wraps a UnaryHandler.
func ChainUnary ¶
func ChainUnary(m ...UnaryMiddleware) UnaryMiddleware
ChainUnary composes unary middleware in declaration order. The first middleware is the outermost wrapper and runs first on entry.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package circuitbreaker provides client middleware that stops calling an operation whose recent attempts have been failing, so a struggling dependency gets headroom to recover instead of more load.
|
Package circuitbreaker provides client middleware that stops calling an operation whose recent attempts have been failing, so a struggling dependency gets headroom to recover instead of more load. |
|
Package governance turns middleware parameters into dynamically observable values instead of construction-time constants.
|
Package governance turns middleware parameters into dynamically observable values instead of construction-time constants. |
|
Package logging provides middleware that writes one structured record per request: operation, transport kind, request summary, latency, and — on failure — the error's kind, reason, domain, and trace ID.
|
Package logging provides middleware that writes one structured record per request: operation, transport kind, request summary, latency, and — on failure — the error's kind, reason, domain, and trace ID. |
|
Package metadata provides middleware that moves application metadata (package github.com/sylphylabs/forge/metadata) between the context and the transport headers, so request-scoped values propagate across process boundaries without transport-specific code.
|
Package metadata provides middleware that moves application metadata (package github.com/sylphylabs/forge/metadata) between the context and the transport headers, so request-scoped values propagate across process boundaries without transport-specific code. |
|
Package ratelimit provides server middleware that sheds load when the service is over capacity, failing rejected requests fast with ErrLimitExceed (KindResourceExhausted) instead of queueing them into timeouts.
|
Package ratelimit provides server middleware that sheds load when the service is over capacity, failing rejected requests fast with ErrLimitExceed (KindResourceExhausted) instead of queueing them into timeouts. |
|
Package recovery provides middleware that recovers a panicking handler, logs the panic value with its stack, and converts the panic into an error the transport can serve.
|
Package recovery provides middleware that recovers a panicking handler, logs the panic value with its stack, and converts the panic into an error the transport can serve. |
|
Package retry provides client middleware that re-invokes a failed unary call, with an injectable backoff curve — exponential full jitter by default — and a per-operation policy that can be governed at runtime.
|
Package retry provides client middleware that re-invokes a failed unary call, with an injectable backoff curve — exponential full jitter by default — and a per-operation policy that can be governed at runtime. |
|
Package selector provides middleware that applies other middleware conditionally, by matching the operation of the call in flight.
|
Package selector provides middleware that applies other middleware conditionally, by matching the operation of the call in flight. |
|
Package throws asserts at runtime that the error identities leaving a method are the ones its Protobuf throws declarations promised.
|
Package throws asserts at runtime that the error identities leaving a method are the ones its Protobuf throws declarations promised. |
|
Package timeout provides server middleware that bounds handler execution time, with a per-operation deadline that can be governed at runtime.
|
Package timeout provides server middleware that bounds handler execution time, with a per-operation deadline that can be governed at runtime. |
|
Package validate provides server middleware that rejects invalid requests before the handler runs.
|
Package validate provides server middleware that rejects invalid requests before the handler runs. |