middleware

package
v0.0.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 24, 2026 License: MIT Imports: 2 Imported by: 0

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

type ServerStream interface {
	Context() context.Context
	SendMsg(any) error
	RecvMsg(any) error
}

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

type UnaryHandler func(ctx context.Context, req any) (any, error)

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL