Architecture
How the request pipeline, middleware chain, and extension lifecycle work in Bastion.
Bastion is organized around a middleware pipeline that processes every request through a series of well-defined stages. The gateway registers itself as a Forge extension and manages routes, upstreams, and resilience features through a unified engine.
Request pipeline
Every request entering the gateway flows through this pipeline. Each stage has a clear responsibility and can short-circuit the chain (for example, a rate limiter returning 429 or a circuit breaker returning 503).
Client Request
│
▼
┌──────────────────────────┐
│ 1. IP Filter │ Allow/deny based on client IP
└──────────┬───────────────┘
│
▼
┌──────────────────────────┐
│ 2. CORS │ Handle preflight requests, set CORS headers
└──────────┬───────────────┘
│
▼
┌──────────────────────────┐
│ 3. Rate Limiter (global)│ Token-bucket check against global limit
└──────────┬───────────────┘
│
▼
┌──────────────────────────┐
│ 4. Route Matcher │ Match request path to a configured route
└──────────┬───────────────┘
│
▼
┌──────────────────────────┐
│ 5. Rate Limiter (route) │ Per-route token-bucket check
└──────────┬───────────────┘
│
▼
┌──────────────────────────┐
│ 6. Authentication │ API key, Bearer token, or forward-auth
└──────────┬───────────────┘
│
▼
┌──────────────────────────┐
│ 7. Response Cache (get) │ Check in-memory cache for a cached response
└──────────┬───────────────┘
│ cache miss
▼
┌──────────────────────────┐
│ 8. Request Hooks │ OnRequest callbacks (header injection, logging)
└──────────┬───────────────┘
│
▼
┌──────────────────────────┐
│ 9. Traffic Splitter │ Canary, blue-green, A/B, shadow routing
└──────────┬───────────────┘
│
▼
┌──────────────────────────┐
│ 10. Load Balancer │ Select upstream target (round-robin, weighted, etc.)
└──────────┬───────────────┘
│
▼
┌──────────────────────────┐
│ 11. Circuit Breaker │ Check target circuit state (closed/open/half-open)
└──────────┬───────────────┘
│
▼
┌──────────────────────────┐
│ 12. TLS / mTLS │ Establish upstream TLS, present client certs
└──────────┬───────────────┘
│
▼
┌──────────────────────────┐
│ 13. Protocol Proxy │ HTTP, WebSocket, SSE, or gRPC proxy
│ + Path Rewriting │ Strip prefix, rewrite path
│ + Header Manipulation│ Add/remove/modify headers
└──────────┬───────────────┘
│
▼
┌──────────────────────────┐
│ 14. Upstream Connection │ Send request, receive response
└──────────┬───────────────┘
│
▼
┌──────────────────────────┐
│ 15. Response Cache (put) │ Store response in cache if policy allows
└──────────┬───────────────┘
│
▼
┌──────────────────────────┐
│ 16. Response Hooks │ OnResponse callbacks (header injection, logging)
└──────────┬───────────────┘
│
▼
┌──────────────────────────┐
│ 17. Access Logger │ Structured log entry with timing, status, route
└──────────┬───────────────┘
│
▼
Client ResponseShort-circuit behavior
Several stages can terminate the pipeline early:
| Stage | Condition | Response |
|---|---|---|
| IP Filter | Client IP is denied | 403 Forbidden |
| Rate Limiter (global) | Token bucket exhausted | 429 Too Many Requests |
| Route Matcher | No matching route | 404 Not Found |
| Rate Limiter (route) | Per-route bucket exhausted | 429 Too Many Requests |
| Authentication | Invalid or missing credentials | 401 Unauthorized |
| Response Cache | Cache hit | Cached response (skips upstream) |
| Circuit Breaker | Circuit is open | 503 Service Unavailable |
Middleware chain
The pipeline is implemented as a chain of HTTP middleware handlers. Each middleware wraps the next, forming a nested call stack. This design allows each stage to:
- Inspect and modify the request before passing it downstream
- Inspect and modify the response on the way back up
- Short-circuit by writing a response directly and not calling the next handler
The order of middleware is fixed and matches the pipeline diagram above. You cannot reorder stages, but you can disable individual stages via configuration (for example, disabling authentication or rate limiting).
Protocol handling
Bastion supports four protocols, each with dedicated proxy logic:
HTTP
Standard reverse proxy using Go's httputil.ReverseProxy with customized transport, error handling, and response modification. Supports HTTP/1.1 and HTTP/2.
WebSocket
WebSocket connections are detected via the Upgrade: websocket header. Bastion performs the WebSocket handshake with the upstream, then bidirectionally copies frames between client and upstream. The connection is maintained until either side closes it.
SSE (Server-Sent Events)
Server-Sent Events are proxied by establishing a long-lived HTTP connection to the upstream and streaming the response body back to the client with chunked transfer encoding and appropriate Content-Type: text/event-stream headers. Bastion disables response buffering for SSE routes.
gRPC
gRPC traffic is proxied at the HTTP/2 level. Bastion forwards gRPC frames (including trailers) between the client and upstream without interpreting the protobuf payload. Both unary and streaming RPCs are supported.
Protocol selection
The protocol is configured per-route via the Protocol field in RouteConfig:
bastion.WithRoute(bastion.RouteConfig{
Path: "/api/*",
Protocol: bastion.ProtocolHTTP, // HTTP proxying
})
bastion.WithRoute(bastion.RouteConfig{
Path: "/ws/*",
Protocol: bastion.ProtocolWebSocket, // WebSocket proxying
})
bastion.WithRoute(bastion.RouteConfig{
Path: "/events/*",
Protocol: bastion.ProtocolSSE, // SSE proxying
})
bastion.WithRoute(bastion.RouteConfig{
Path: "/grpc/*",
Protocol: bastion.ProtocolGRPC, // gRPC proxying
})For HTTP routes, Bastion also auto-detects WebSocket upgrade requests and handles them transparently.
Extension lifecycle
Bastion implements the Forge extension lifecycle with four phases:
┌─────────────────────┐
│ 1. Register │ Parse configuration (YAML or programmatic)
│ │ Build route table from static routes
│ │ Initialize middleware chain
│ │ Mount HTTP handlers on the Forge router
│ │ Provide gateway instance to DI container
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ 2. Start │ Start health check probes
│ │ Start FARP service discovery (if enabled)
│ │ Start configuration file watcher (if enabled)
│ │ Open WebSocket event stream (if dashboard enabled)
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ 3. Health │ Report gateway health status
│ │ Check upstream connectivity
│ │ Report route and target counts
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ 4. Stop │ Drain in-flight requests
│ │ Stop health check probes
│ │ Stop service discovery watcher
│ │ Close WebSocket connections
│ │ Release resources
└─────────────────────┘Hook system
Bastion provides lifecycle hooks that let you extend gateway behavior without modifying the core pipeline. Hooks are registered on the gateway instance and called at specific points in the request lifecycle.
OnRequest
Called after route matching and authentication, before the request is forwarded to the upstream. Use this to inject headers, log requests, or modify the request.
gw.Hooks().OnRequest(func(r *http.Request, route *bastion.Route) error {
r.Header.Set("X-Gateway-Auth", "validated")
return nil
})Returning a non-nil error from OnRequest aborts the request and triggers the OnError hook.
OnResponse
Called after the upstream responds, before the response is sent to the client. Use this to modify response headers or log responses.
gw.Hooks().OnResponse(func(resp *http.Response, route *bastion.Route) {
resp.Header.Set("X-Served-By", "bastion")
})OnError
Called when an error occurs during proxying (upstream timeout, connection refused, circuit breaker open, etc.). Use this for custom error logging or error page rendering.
gw.Hooks().OnError(func(err error, route *bastion.Route, w http.ResponseWriter) {
log.Printf("Gateway error on route %s: %v", route.Path, err)
})OnRouteChange
Called when a route is added, updated, or removed (via the admin API or service discovery).
gw.Hooks().OnRouteChange(func(event bastion.RouteChangeEvent) {
log.Printf("Route %s: %s", event.Action, event.Route.Path)
})OnUpstreamHealth
Called when an upstream target changes health status (healthy to unhealthy or vice versa).
gw.Hooks().OnUpstreamHealth(func(target *bastion.Target, healthy bool) {
log.Printf("Target %s is now healthy=%v", target.URL, healthy)
})Package structure
| Package | Import path | Purpose |
|---|---|---|
bastion | github.com/xraph/bastion | Root -- extension constructor, configuration types, option functions |
proxy | github.com/xraph/bastion/proxy | Protocol-aware reverse proxy engine (HTTP, WS, SSE, gRPC) |
resilience | github.com/xraph/bastion/resilience | Circuit breakers, retry logic, backoff strategies |
health | github.com/xraph/bastion/health | Active health probes and passive failure tracking |
discovery | github.com/xraph/bastion/discovery | FARP service discovery integration and OpenAPI aggregation |
security | github.com/xraph/bastion/security | Authentication (API key, Bearer, forward-auth), IP filtering, CORS |
api | github.com/xraph/bastion/api | Admin REST API handlers and WebSocket event stream |
store | github.com/xraph/bastion/store | Route and configuration persistence backends |
config | github.com/xraph/bastion/config | Configuration parsing, validation, and hot-reload watcher |