Documentation
¶
Index ¶
- Constants
- Variables
- func CORS(config CORSConfig) forge.Middleware
- func ClientIP(r *http.Request) string
- func Compress(level int) forge.Middleware
- func CompressDefault() forge.Middleware
- func DefaultIdempotencyPrincipal(ctx forge.Context) string
- func GetRequestID(ctx context.Context) string
- func GetRequestIDFromForgeContext(ctx forge.Context) string
- func Idempotency(store IdempotencyStore, opts ...IdempotencyOption) forge.Middleware
- func Logging(logger forge.Logger) forge.Middleware
- func LoggingWithConfig(logger forge.Logger, config LoggingConfig) forge.Middleware
- func RateLimit(limiter *RateLimiter, logger forge.Logger) forge.Middleware
- func RateLimitWithKey(limiter *RateLimiter, keyFunc RateLimitKeyFunc, logger forge.Logger) forge.Middleware
- func Recovery(logger forge.Logger) forge.Middleware
- func RequestID() forge.Middleware
- func Timeout(duration time.Duration, logger forge.Logger) func(http.Handler) http.Handler
- type CORSConfig
- type IdempotencyBegun
- type IdempotencyConflictMode
- type IdempotencyKey
- type IdempotencyOption
- func IdempotencyAllowAnonymous() IdempotencyOption
- func IdempotencyClock(now func() time.Time) IdempotencyOption
- func IdempotencyLease(d time.Duration) IdempotencyOption
- func IdempotencyLogger(l forge.Logger) IdempotencyOption
- func IdempotencyMaxBody(n int64) IdempotencyOption
- func IdempotencyMaxResponse(n int) IdempotencyOption
- func IdempotencyOnConflict(mode IdempotencyConflictMode) IdempotencyOption
- func IdempotencyPrincipal(fn IdempotencyPrincipalFunc) IdempotencyOption
- func IdempotencyRequireKey() IdempotencyOption
- func IdempotencyTTL(d time.Duration) IdempotencyOption
- func IdempotencyWaitTimeout(d time.Duration) IdempotencyOption
- type IdempotencyPrincipalFunc
- type IdempotencyState
- type IdempotencyStore
- type IdempotencyToken
- type IdempotentResponse
- type LoggingConfig
- type MemoryIdempotencyOption
- type MemoryIdempotencyStore
- type RateLimitKeyFunc
- type RateLimiter
- type RequestIDContextKey
- type ResponseWriter
- func (w *ResponseWriter) Flush()
- func (w *ResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error)
- func (w *ResponseWriter) Push(target string, opts *http.PushOptions) error
- func (w *ResponseWriter) Size() int
- func (w *ResponseWriter) Status() int
- func (w *ResponseWriter) Write(data []byte) (int, error)
- func (w *ResponseWriter) WriteHeader(code int)
- func (w *ResponseWriter) Written() bool
Examples ¶
Constants ¶
const ( // IdempotencyAcquired means the caller now holds the key. IdempotencyAcquired = idempotency.Acquired // IdempotencyReplay means a stored response exists. IdempotencyReplay = idempotency.Replay // IdempotencyInFlight means another request holds the key. IdempotencyInFlight = idempotency.InFlight )
const ( // IdempotencyWait makes a concurrent duplicate wait, then replay. IdempotencyWait = idempotency.ConflictWait // IdempotencyReject answers a concurrent duplicate with 409. IdempotencyReject = idempotency.ConflictReject )
const DefaultIdempotencyMaxEntries = idempotency.DefaultMaxEntries
DefaultIdempotencyMaxEntries is the in-memory store's default cap.
const DefaultTimeoutBufferLimit = 1 << 20 // 1 MiB
DefaultTimeoutBufferLimit bounds how much of a response the Timeout middleware will hold in memory before it gives up on buffering and starts passing writes straight through. Buffering exists so a timeout can replace a partially written response; past this size that is no longer worth the memory, and an unbounded buffer would let any large response drive allocation.
const IdempotencyKeyHeader = idempotency.HeaderName
IdempotencyKeyHeader is the request header the middleware reads.
const IdempotencySkippedHeader = idempotency.SkippedHeader
IdempotencySkippedHeader is set on the response to a request whose Idempotency-Key the middleware did not act on. "anonymous" means the request had no principal, so it ran without deduplication.
const IdempotentReplayedHeader = idempotency.ReplayedHeader
IdempotentReplayedHeader is set to "true" on a replayed response.
const IdempotentTruncatedHeader = idempotency.TruncatedHeader
IdempotentTruncatedHeader is set to "true" on a replay of a response larger than IdempotencyMaxResponse: the status and headers are replayed, the body is empty.
const RequestIDForgeKey = "request_id"
RequestIDForgeKey is the forge-context key under which the request ID is stored. Exported so callers can read it without guessing the string.
Variables ¶
var ErrIdempotencyNotHolder = idempotency.ErrNotHolder
ErrIdempotencyNotHolder is what Complete and Release return when the token does not match the claim on the key. The store must leave the key untouched.
Functions ¶
func CORS ¶
func CORS(config CORSConfig) forge.Middleware
CORS returns middleware that handles Cross-Origin Resource Sharing.
Example (Development) ¶
ExampleCORS_development shows a permissive CORS configuration for development.
package main
import (
"net/http"
"github.com/xraph/forge"
"github.com/xraph/forge/middleware"
)
func main() {
app := forge.New()
router := app.Router()
// Development: Allow all origins (NOT for production)
cors := middleware.CORS(middleware.DefaultCORSConfig())
router.Use(cors)
router.GET("/api/users", func(ctx forge.Context) error {
return ctx.JSON(http.StatusOK, map[string]string{"user": "john"})
})
_ = app.Run()
}
Output:
Example (MultipleOrigins) ¶
ExampleCORS_multipleOrigins shows configuration with multiple specific origins.
package main
import (
"net/http"
"github.com/xraph/forge"
"github.com/xraph/forge/middleware"
)
func main() {
app := forge.New()
router := app.Router()
corsConfig := middleware.CORSConfig{
AllowOrigins: []string{
"https://web.example.com",
"https://mobile.example.com",
"https://partner.acme.com",
},
AllowMethods: []string{"GET", "POST", "PATCH"},
AllowHeaders: []string{"Content-Type", "Authorization", "X-API-Key"},
ExposeHeaders: []string{"X-Request-ID"},
AllowCredentials: true,
MaxAge: 3600,
}
cors := middleware.CORS(corsConfig)
router.Use(cors)
router.POST("/api/orders", func(ctx forge.Context) error {
return ctx.JSON(http.StatusCreated, map[string]string{"id": "123"})
})
_ = app.Run()
}
Output:
Example (Production) ¶
ExampleCORS_production shows a secure CORS configuration for production.
package main
import (
"net/http"
"github.com/xraph/forge"
"github.com/xraph/forge/middleware"
)
func main() {
app := forge.New()
router := app.Router()
// Production: Specific origins with credentials
corsConfig := middleware.CORSConfig{
AllowOrigins: []string{
"https://app.example.com",
"https://admin.example.com",
},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},
AllowHeaders: []string{"Content-Type", "Authorization"},
ExposeHeaders: []string{"X-Request-ID", "X-Total-Count"},
AllowCredentials: true, // Enable cookies/auth headers
MaxAge: 7200, // 2 hours
}
cors := middleware.CORS(corsConfig)
router.Use(cors)
router.GET("/api/users", func(ctx forge.Context) error {
return ctx.JSON(http.StatusOK, map[string]string{"user": "john"})
})
_ = app.Run()
}
Output:
Example (SpecificRoutes) ¶
ExampleCORS_specificRoutes shows applying CORS to specific route groups.
package main
import (
"net/http"
"github.com/xraph/forge"
"github.com/xraph/forge/middleware"
)
func main() {
app := forge.New()
router := app.Router()
// Public API: Permissive CORS
publicCORS := middleware.CORS(middleware.CORSConfig{
AllowOrigins: []string{"*"},
AllowMethods: []string{"GET"},
AllowHeaders: []string{"Content-Type"},
AllowCredentials: false,
MaxAge: 3600,
})
// Private API: Strict CORS with credentials
privateCORS := middleware.CORS(middleware.CORSConfig{
AllowOrigins: []string{"https://app.example.com"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},
AllowHeaders: []string{"Content-Type", "Authorization"},
AllowCredentials: true,
MaxAge: 3600,
})
// Public routes
public := router.Group("/public")
public.Use(publicCORS)
public.GET("/status", func(ctx forge.Context) error {
return ctx.JSON(http.StatusOK, map[string]string{"status": "ok"})
})
// Private routes
private := router.Group("/api")
private.Use(privateCORS)
private.GET("/users", func(ctx forge.Context) error {
return ctx.JSON(http.StatusOK, map[string]string{"user": "john"})
})
_ = app.Run()
}
Output:
Example (WildcardSubdomain) ¶
ExampleCORS_wildcardSubdomain shows wildcard subdomain support.
package main
import (
"net/http"
"github.com/xraph/forge"
"github.com/xraph/forge/middleware"
)
func main() {
app := forge.New()
router := app.Router()
// Allow all subdomains of example.com
corsConfig := middleware.CORSConfig{
AllowOrigins: []string{"*.example.com"},
AllowMethods: []string{"GET", "POST"},
AllowHeaders: []string{"Content-Type"},
AllowCredentials: false,
MaxAge: 3600,
}
cors := middleware.CORS(corsConfig)
router.Use(cors)
router.GET("/api/data", func(ctx forge.Context) error {
return ctx.JSON(http.StatusOK, map[string]string{"data": "value"})
})
_ = app.Run()
}
Output:
func ClientIP ¶ added in v1.9.0
ClientIP returns the client's IP address from RemoteAddr, discarding the port.
This is the default rate-limit key. Using RemoteAddr verbatim does not work: Go's HTTP server sets it to "IP:ephemeral-port" and the port changes per connection, so every connection would get a fresh bucket — no effective limit at all, plus unbounded map growth.
This deliberately ignores X-Forwarded-For. Behind a proxy every client collapses into the proxy's IP, so supply a RateLimitKeyFunc that reads the forwarded header only for hops you actually trust — reading it unconditionally is a header-spoofing bypass.
func Compress ¶
func Compress(level int) forge.Middleware
Compress middleware compresses HTTP responses using gzip Only compresses if client supports gzip and response is suitable for compression This is the new forge middleware pattern using forge.Handler.
func CompressDefault ¶
func CompressDefault() forge.Middleware
CompressDefault returns Compress middleware with default compression level.
func DefaultIdempotencyPrincipal ¶ added in v1.12.2
DefaultIdempotencyPrincipal reads "auth.subject", then the Subject of "auth_context" (as "provider:subject" when its ProviderName is set), and returns "" for an anonymous request.
func GetRequestID ¶
GetRequestID retrieves the request ID from standard context.
func GetRequestIDFromForgeContext ¶ added in v0.5.0
GetRequestIDFromForgeContext retrieves the request ID from Forge context.
func Idempotency ¶ added in v1.12.2
func Idempotency(store IdempotencyStore, opts ...IdempotencyOption) forge.Middleware
Idempotency runs a write handler at most once per principal, METHOD /path and Idempotency-Key, and replays the stored response to every repeat.
A repeat with a different body gets 422. A repeat that arrives while the first is still running waits for it (or gets 409 with IdempotencyOnConflict(IdempotencyReject)); that 409 carries Retry-After: 1. A handler that returns an error, panics or answers 408, 429 or 5xx stores nothing, so the client's retry runs it again. A 4xx the handler writes itself is stored, while a 4xx returned as an error is not, because the error handler writes it outside this middleware. A response larger than IdempotencyMaxResponse is replayed without its body, marked Idempotent-Truncated: true. GET, HEAD and OPTIONS and writes without the header pass straight through. So does a keyed request without a principal (unless IdempotencyAllowAnonymous is set), undeduplicated: its response carries Idempotency-Skipped: anonymous, and the first one on each route is logged as a warning, because the usual cause is auth registered after this middleware.
The handler runs on a fresh context that shares the outer context's values and session, so ctx.Get and ctx.Session work behind this middleware. Other private state of the outer context, such as a DI scope it opened, is not carried across.
Prefer forge.WithIdempotency on the route: it installs this middleware and also marks the operation x-forge-idempotent so generated clients know a replay is safe.
func Logging ¶
func Logging(logger forge.Logger) forge.Middleware
Logging middleware logs HTTP requests with timing information.
func LoggingWithConfig ¶
func LoggingWithConfig(logger forge.Logger, config LoggingConfig) forge.Middleware
LoggingWithConfig middleware logs HTTP requests with custom configuration.
func RateLimit ¶
func RateLimit(limiter *RateLimiter, logger forge.Logger) forge.Middleware
RateLimit middleware enforces rate limiting per client, keyed on client IP.
func RateLimitWithKey ¶ added in v1.9.0
func RateLimitWithKey(limiter *RateLimiter, keyFunc RateLimitKeyFunc, logger forge.Logger) forge.Middleware
RateLimitWithKey enforces rate limiting using a caller-supplied key function. Use this to key on an authenticated user ID, an API key, or a trusted forwarded-for hop instead of the client IP.
func Recovery ¶
func Recovery(logger forge.Logger) forge.Middleware
Recovery middleware recovers from panics and logs them with a stack trace. Returns http.StatusInternalServerError on panic.
http.ErrAbortHandler is deliberately re-panicked: net/http uses it as the signal to drop the connection without logging, and swallowing it here would turn an intentional abort into a bogus 500.
func RequestID ¶
func RequestID() forge.Middleware
RequestID middleware adds a unique request ID to each request If X-Request-ID header is present and well-formed, it is used; otherwise a new UUID is generated.
Types ¶
type CORSConfig ¶
type CORSConfig struct {
// AllowOrigins defines the allowed origin(s)
// Use []string{"*"} for all origins (not compatible with AllowCredentials=true)
// Or specific origins like []string{"https://example.com", "https://app.example.com"}
AllowOrigins []string
// AllowMethods defines allowed HTTP methods
AllowMethods []string
// AllowHeaders defines allowed request headers
AllowHeaders []string
// ExposeHeaders defines headers exposed to the client
ExposeHeaders []string
// AllowCredentials indicates whether credentials are allowed
// Cannot be true when AllowOrigins contains "*"
AllowCredentials bool
// MaxAge indicates how long preflight requests can be cached (seconds)
MaxAge int
}
CORSConfig defines configuration for CORS middleware.
func DefaultCORSConfig ¶
func DefaultCORSConfig() CORSConfig
DefaultCORSConfig returns a permissive CORS configuration suitable for development.
type IdempotencyBegun ¶ added in v1.12.2
type IdempotencyBegun = idempotency.Begun
IdempotencyBegun is what IdempotencyStore.Begin found.
type IdempotencyConflictMode ¶ added in v1.12.2
type IdempotencyConflictMode = idempotency.ConflictMode
IdempotencyConflictMode selects waiting or 409 for a concurrent duplicate.
type IdempotencyKey ¶ added in v1.12.2
type IdempotencyKey = idempotency.Key
IdempotencyKey identifies one idempotent operation: who, where, which key.
type IdempotencyOption ¶ added in v1.12.2
type IdempotencyOption = idempotency.Option
IdempotencyOption configures Idempotency and forge.WithIdempotency.
func IdempotencyAllowAnonymous ¶ added in v1.12.2
func IdempotencyAllowAnonymous() IdempotencyOption
IdempotencyAllowAnonymous deduplicates requests without a principal too. By default such a request runs as if it carried no Idempotency-Key, because every anonymous caller shares the empty principal, and its response carries Idempotency-Skipped: anonymous.
func IdempotencyClock ¶ added in v1.12.2
func IdempotencyClock(now func() time.Time) IdempotencyOption
IdempotencyClock replaces the middleware's clock, for tests.
func IdempotencyLease ¶ added in v1.12.2
func IdempotencyLease(d time.Duration) IdempotencyOption
IdempotencyLease bounds how long an unfinished request holds its key. Default 1m.
func IdempotencyLogger ¶ added in v1.12.2
func IdempotencyLogger(l forge.Logger) IdempotencyOption
IdempotencyLogger is where the middleware warns about a misconfiguration, such as keyed requests arriving with no principal. Default: the application logger in the request's container.
func IdempotencyMaxBody ¶ added in v1.12.2
func IdempotencyMaxBody(n int64) IdempotencyOption
IdempotencyMaxBody caps the request body hashed for the fingerprint. A larger body on a keyed request gets 413. Default 1 MiB.
func IdempotencyMaxResponse ¶ added in v1.12.2
func IdempotencyMaxResponse(n int) IdempotencyOption
IdempotencyMaxResponse caps the response body stored for replay. A larger response is sent in full and replayed with its status and headers, an empty body and Idempotent-Truncated: true. Default 1 MiB.
func IdempotencyOnConflict ¶ added in v1.12.2
func IdempotencyOnConflict(mode IdempotencyConflictMode) IdempotencyOption
IdempotencyOnConflict selects waiting or 409 for a concurrent duplicate.
func IdempotencyPrincipal ¶ added in v1.12.2
func IdempotencyPrincipal(fn IdempotencyPrincipalFunc) IdempotencyOption
IdempotencyPrincipal replaces DefaultIdempotencyPrincipal.
func IdempotencyRequireKey ¶ added in v1.12.2
func IdempotencyRequireKey() IdempotencyOption
IdempotencyRequireKey answers 400 to a write without an Idempotency-Key. A keyed write without a principal still passes through undeduplicated, with Idempotency-Skipped: anonymous on its response.
func IdempotencyTTL ¶ added in v1.12.2
func IdempotencyTTL(d time.Duration) IdempotencyOption
IdempotencyTTL is how long a response is replayed. Default 24h.
func IdempotencyWaitTimeout ¶ added in v1.12.2
func IdempotencyWaitTimeout(d time.Duration) IdempotencyOption
IdempotencyWaitTimeout bounds how long a concurrent duplicate waits. Default 10s.
type IdempotencyPrincipalFunc ¶ added in v1.12.2
type IdempotencyPrincipalFunc = idempotency.PrincipalFunc
IdempotencyPrincipalFunc names who made a request.
type IdempotencyState ¶ added in v1.12.2
type IdempotencyState = idempotency.State
IdempotencyState is the State in IdempotencyBegun.
type IdempotencyStore ¶ added in v1.12.2
type IdempotencyStore = idempotency.Store
IdempotencyStore remembers one response per Idempotency-Key. Begin claims a key or reports a stored or running request, Complete stores a response, Release gives the key back. Implementations must be safe for concurrent use.
type IdempotencyToken ¶ added in v1.12.2
type IdempotencyToken = idempotency.Token
IdempotencyToken proves which Begin holds a key. A store hands one out with IdempotencyAcquired and checks it on Complete and Release, so a holder whose lease lapsed cannot finish a claim that has passed to another request. A custom IdempotencyStore needs this name to declare Complete and Release.
type IdempotentResponse ¶ added in v1.12.2
type IdempotentResponse = idempotency.Response
IdempotentResponse is a stored response.
type LoggingConfig ¶
type LoggingConfig struct {
// IncludeHeaders includes request headers in logs
IncludeHeaders bool
// ExcludePaths defines paths to exclude from logging
ExcludePaths []string
// SensitiveHeaders defines headers to redact in logs
SensitiveHeaders []string
}
LoggingConfig defines configuration for logging middleware.
func DefaultLoggingConfig ¶
func DefaultLoggingConfig() LoggingConfig
DefaultLoggingConfig returns default logging configuration.
type MemoryIdempotencyOption ¶ added in v1.12.2
type MemoryIdempotencyOption = idempotency.MemoryOption
MemoryIdempotencyOption configures a MemoryIdempotencyStore.
func MemoryIdempotencyClock ¶ added in v1.12.2
func MemoryIdempotencyClock(now func() time.Time) MemoryIdempotencyOption
MemoryIdempotencyClock replaces the store's clock, for tests.
func MemoryIdempotencyMaxEntries ¶ added in v1.12.2
func MemoryIdempotencyMaxEntries(n int) MemoryIdempotencyOption
MemoryIdempotencyMaxEntries caps how many responses the store keeps.
type MemoryIdempotencyStore ¶ added in v1.12.2
type MemoryIdempotencyStore = idempotency.MemoryStore
MemoryIdempotencyStore is the in-memory IdempotencyStore.
func NewMemoryIdempotencyStore ¶ added in v1.12.2
func NewMemoryIdempotencyStore(opts ...MemoryIdempotencyOption) *MemoryIdempotencyStore
NewMemoryIdempotencyStore returns an in-memory IdempotencyStore.
type RateLimitKeyFunc ¶ added in v1.9.0
RateLimitKeyFunc derives the bucket key for a request. Return "" to skip rate limiting for that request.
type RateLimiter ¶
type RateLimiter struct {
// contains filtered or unexported fields
}
RateLimiter implements token bucket algorithm for rate limiting.
func NewRateLimiter ¶
func NewRateLimiter(rate, burst int) *RateLimiter
NewRateLimiter creates a new rate limiter rate: maximum requests per second burst: maximum burst size (capacity)
Call Stop when the limiter is no longer needed to release its cleanup goroutine.
func (*RateLimiter) Allow ¶
func (rl *RateLimiter) Allow(key string) bool
Allow checks if a request from the given key should be allowed.
func (*RateLimiter) SetMaxBuckets ¶ added in v1.9.0
func (rl *RateLimiter) SetMaxBuckets(maxBuckets int)
SetMaxBuckets overrides how many distinct keys are tracked before new keys are rejected. Values below 1 are ignored.
func (*RateLimiter) Stop ¶ added in v1.9.0
func (rl *RateLimiter) Stop()
Stop terminates the cleanup goroutine. Safe to call more than once.
type RequestIDContextKey ¶
type RequestIDContextKey string
RequestIDContextKey is the context key for storing request ID.
type ResponseWriter ¶
type ResponseWriter struct {
http.ResponseWriter
// contains filtered or unexported fields
}
ResponseWriter wraps http.ResponseWriter to capture status code and size.
func NewResponseWriter ¶
func NewResponseWriter(w http.ResponseWriter) *ResponseWriter
NewResponseWriter creates a new ResponseWriter.
func (*ResponseWriter) Hijack ¶
func (w *ResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error)
Hijack implements http.Hijacker.
func (*ResponseWriter) Push ¶
func (w *ResponseWriter) Push(target string, opts *http.PushOptions) error
Push implements http.Pusher.
func (*ResponseWriter) Size ¶
func (w *ResponseWriter) Size() int
Size returns the response size in bytes.
func (*ResponseWriter) Status ¶
func (w *ResponseWriter) Status() int
Status returns the HTTP status code.
func (*ResponseWriter) Write ¶
func (w *ResponseWriter) Write(data []byte) (int, error)
Write writes data and captures size.
func (*ResponseWriter) WriteHeader ¶
func (w *ResponseWriter) WriteHeader(code int)
WriteHeader captures the status code.
func (*ResponseWriter) Written ¶ added in v1.9.0
func (w *ResponseWriter) Written() bool
Written reports whether a status code has been committed to the client. Recovery uses this to avoid writing a second header over a partial response.