diff --git a/adaptive/doc.go b/adaptive/doc.go index 1e027cc2..3ffbf0d7 100644 --- a/adaptive/doc.go +++ b/adaptive/doc.go @@ -11,4 +11,8 @@ // // Users select the adaptive engine via celeris.Config{Engine: celeris.Adaptive}. // It is the default engine on Linux. +// +// # Documentation +// +// Full guides and examples: https://goceleris.dev/docs/engines package adaptive diff --git a/doc.go b/doc.go index d89c7029..bc4974b8 100644 --- a/doc.go +++ b/doc.go @@ -1,624 +1,41 @@ -// Package celeris provides an ultra-low latency HTTP server with -// dual-architecture I/O (io_uring + epoll) and a high-level API -// for routing and request handling. +// Package celeris is a high-performance HTTP server with dual-architecture +// I/O (io_uring + epoll) and a high-level API for routing and request handling. // -// # Quick Start +// Create a server with [New], register routes with the verb methods +// ([Server.GET], [Server.POST], …), and serve with [Server.Start] (or +// [Server.StartWithContext] for graceful shutdown). Routes support static +// paths, named parameters (:id) and catch-all wildcards (*path). // // s := celeris.New(celeris.Config{Addr: ":8080"}) -// s.GET("/hello", func(c *celeris.Context) error { -// return c.String(200, "Hello, World!") +// s.GET("/users/:id", func(c *celeris.Context) error { +// return c.JSON(200, map[string]string{"id": c.Param("id")}) // }) // log.Fatal(s.Start()) // -// # Routing +// Handlers have the signature [HandlerFunc] (func(*[Context]) error) and +// receive a pooled *[Context] that carries the request and response: params +// ([Context.Param]), query/form/body parsing ([Context.Query], +// [Context.Bind]), and typed responses ([Context.JSON], [Context.String], +// [Context.File], [Context.Stream]). Do not retain a *[Context] after the +// handler returns; use [Context.BodyCopy] to keep body bytes alive. // -// Routes support static paths, named parameters, and catch-all wildcards: +// Group routes with [Server.Group], attach middleware with [Server.Use], +// [RouteGroup.Use] or [Route.Use], and return [HTTPError] (via [NewHTTPError]) +// for HTTP-status errors. The in-tree middleware/* packages (logger, recovery, +// cors, compress, …) supply ready-made [HandlerFunc] middleware. // -// s.GET("/users/:id", handler) // /users/42 → Param("id") = "42" -// s.GET("/files/*path", handler) // /files/a/b → Param("path") = "/a/b" +// By default handlers run inline on the I/O worker; mark I/O-bound routes with +// [Route.Async] (or flip the default via [Config.AsyncHandlers]) to dispatch +// them on a per-connection goroutine. // -// # URL Parameters +// On Linux, [Config.Engine] selects between [IOUring], [Epoll], [Adaptive] and +// [Std]; other platforms run [Std] only. [Config.Protocol] selects [HTTP1], +// [H2C] or [Auto] (HTTP/1.1 + h2c). [Server.Collector] exposes per-request +// metrics. Test handlers with the github.com/goceleris/celeris/celeristest +// helpers. // -// Access matched parameters by name. Type-safe parsing methods are available: +// # Documentation // -// id := c.Param("id") // string -// n, err := c.ParamInt("id") // int -// n64, err := c.ParamInt64("id") // int64 -// -// Query parameters support defaults and multi-values: -// -// page := c.Query("page") // string -// page := c.QueryDefault("page", "1") // with default -// limit := c.QueryInt("limit", 10) // int with default -// tags := c.QueryValues("tag") // []string -// all := c.QueryParams() // url.Values -// raw := c.RawQuery() // raw query string -// -// # Route Groups -// -// api := s.Group("/api") -// api.GET("/items", listItems) -// -// # Async Handlers -// -// By default every handler runs inline on the I/O worker — lowest latency, -// zero handoff. Routes that block on I/O (database, RPC, file system) can -// opt per-route into the per-connection dispatch goroutine so the worker -// stays free to drive other connections: -// -// s.GET("/healthz", healthHandler) // sync (default), inline on worker -// s.GET("/db", dbHandler).Async() // async, per-conn goroutine -// -// Or flip the default at the group / server level and opt routes back to -// sync individually: -// -// api := s.Group("/api").Async() // async for all /api/* routes -// api.GET("/products", productHandler) -// api.GET("/cached", cachedHandler).Sync() // opt this one back to sync -// -// Precedence is route > group > server default ([Config.AsyncHandlers]). -// Works identically across iouring, epoll and adaptive (per-request H1 -// dispatch, per-stream H2 dispatch). On the std engine the per-route flag -// is a no-op (net/http already runs handler-per-goroutine). -// -// SAFETY: do NOT call .Sync() / .Async(false) on a handler that hijacks -// or detaches the connection (WebSocket upgrade, SSE). Detached flows run -// async by construction. -// -// # Middleware -// -// Middleware is provided by the in-tree middleware/ packages (e.g. -// middleware/logger, middleware/recovery). Use Server.Use to register -// middleware globally or per route group. -// -// import ( -// "github.com/goceleris/celeris/middleware/logger" -// "github.com/goceleris/celeris/middleware/recovery" -// ) -// s.Use(logger.New(), recovery.New()) -// -// To write custom middleware, use the HandlerFunc signature and call -// Context.Next to invoke downstream handlers. Next returns the first -// error from downstream, which middleware can handle or propagate: -// -// func timing() celeris.HandlerFunc { -// return func(c *celeris.Context) error { -// err := c.Next() -// elapsed := time.Since(c.StartTime()) -// c.SetHeader("x-response-time", elapsed.String()) -// return err -// } -// } -// -// # Error Handling -// -// Handlers return errors. Unhandled errors are caught by the routerAdapter -// safety net: *HTTPError writes its Code+Message; bare errors write 500. -// -// s.GET("/data", func(c *celeris.Context) error { -// data, err := fetchData() -// if err != nil { -// return celeris.NewHTTPError(500, "fetch failed") -// } -// return c.JSON(200, data) -// }) -// -// Middleware can intercept errors from downstream handlers: -// -// s.Use(func(c *celeris.Context) error { -// err := c.Next() -// if err != nil { -// log.Println("error:", err) -// return c.JSON(500, map[string]string{"error": "internal"}) -// } -// return nil -// }) -// -// # Global Error Handler -// -// Register a global error handler with Server.OnError. This is called when -// an unhandled error reaches the safety net after all middleware has had its -// chance. Use it to render structured error responses (e.g. JSON) instead of -// the default text/plain fallback: -// -// s.OnError(func(c *celeris.Context, err error) { -// var he *celeris.HTTPError -// code := 500 -// msg := "internal server error" -// if errors.As(err, &he) { -// code = he.Code -// msg = he.Message -// } -// c.JSON(code, map[string]string{"error": msg}) -// }) -// -// If the handler does not write a response, the default text/plain fallback -// applies. OnError must be called before Start. -// -// # Custom 404 / 405 Handlers -// -// s.NotFound(func(c *celeris.Context) error { -// return c.JSON(404, map[string]string{"error": "not found"}) -// }) -// s.MethodNotAllowed(func(c *celeris.Context) error { -// return c.JSON(405, map[string]string{"error": "method not allowed"}) -// }) -// -// # Graceful Shutdown -// -// ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) -// defer stop() -// -// s := celeris.New(celeris.Config{ -// Addr: ":8080", -// ShutdownTimeout: 10 * time.Second, -// }) -// s.GET("/ping", func(c *celeris.Context) error { -// return c.String(200, "pong") -// }) -// if err := s.StartWithContext(ctx); err != nil { -// log.Fatal(err) -// } -// -// # Engine Selection -// -// On Linux, choose between IOUring, Epoll, Adaptive, or Std engines. -// On other platforms, only Std is available. -// -// s := celeris.New(celeris.Config{ -// Addr: ":8080", -// Engine: celeris.Adaptive, -// }) -// -// # Protocol Selection -// -// The Protocol field controls HTTP version negotiation: -// -// celeris.HTTP1 // HTTP/1.1 only (default) -// celeris.H2C // HTTP/2 cleartext (h2c) only -// celeris.Auto // Auto-detect: serves both HTTP/1.1 and H2C -// -// Example: -// -// s := celeris.New(celeris.Config{ -// Addr: ":8080", -// Protocol: celeris.Auto, -// }) -// -// # net/http Compatibility -// -// Wrap existing net/http handlers. Response bodies from adapted handlers -// are buffered in memory (capped at 100 MB). -// -// s.GET("/legacy", celeris.Adapt(legacyHandler)) -// -// # Context Lifecycle -// -// Context objects are pooled and recycled between requests. Do not retain -// references to a *[Context] after the handler returns. Copy any needed -// values before returning. For the request body specifically, use -// Context.BodyCopy to obtain a copy that outlives the handler: -// -// safe := c.BodyCopy() // safe to pass to a goroutine -// -// When using Detach, the returned done function MUST be called — failure -// to do so permanently leaks the Context from the pool. -// -// # Observability -// -// The Server.Collector method returns an [observe.Collector] that records -// per-request metrics (throughput, latency histogram, error rate, active -// connections). Use Collector.Snapshot to retrieve a point-in-time copy: -// -// snap := s.Collector().Snapshot() -// fmt.Println(snap.RequestsTotal, snap.ErrorsTotal) -// -// For Prometheus or debug endpoint integration, see the -// middleware/metrics and middleware/debug packages. -// -// # Configuration -// -// Config.Workers controls the number of I/O workers (default: GOMAXPROCS). -// Config.ShutdownTimeout sets the graceful shutdown deadline for -// StartWithContext (default: 30s). -// -// # Named Routes & Reverse URLs -// -// Assign names to routes with Route.Name, then generate URLs via Server.URL: -// -// s.GET("/users/:id", handler).Name("user") -// url, _ := s.URL("user", "42") // "/users/42" -// -// For catch-all routes the value replaces the wildcard segment: -// -// s.GET("/files/*filepath", handler).Name("files") -// url, _ := s.URL("files", "/css/style.css") // "/files/css/style.css" -// -// Use Server.Routes to list all registered routes. -// -// # Form Handling -// -// Parse url-encoded and multipart form bodies: -// -// name := c.FormValue("name") -// all := c.FormValues("tags") -// -// For file uploads, use FormFile or MultipartForm: -// -// file, header, err := c.FormFile("avatar") -// defer file.Close() -// -// # File Serving -// -// Serve static files with automatic content-type detection and Range support: -// -// s.GET("/download", func(c *celeris.Context) error { -// return c.File("/var/data/report.pdf") -// }) -// -// Callers must sanitize user-supplied paths to prevent directory traversal. -// -// # Static File Serving -// -// Serve an entire directory under a URL prefix: -// -// s.Static("/assets", "./public") -// -// This is equivalent to: -// -// s.GET("/assets/*filepath", func(c *celeris.Context) error { -// return c.FileFromDir("./public", c.Param("filepath")) -// }) -// -// # Streaming -// -// For simple cases, stream an io.Reader as a buffered response (capped at 100 MB): -// -// return c.Stream(200, "text/plain", reader) -// -// For true incremental streaming (SSE, chunked responses), use StreamWriter -// with the Detach pattern: -// -// s.GET("/events", func(c *celeris.Context) error { -// sw := c.StreamWriter() -// if sw == nil { -// return c.String(200, "streaming not supported") -// } -// done := c.Detach() -// sw.WriteHeader(200, [][2]string{{"content-type", "text/event-stream"}}) -// go func() { -// defer done() -// defer sw.Close() -// for event := range events { -// sw.Write([]byte("data: " + event + "\n\n")) -// sw.Flush() -// } -// }() -// return nil -// }) -// -// StreamWriter returns nil if the response is currently being buffered by -// an upstream middleware (e.g. compress, etag); call StreamWriter before -// any middleware buffers, or use [Context.BufferDepth] to detect the -// state. All shipped engines (std, epoll, io_uring) support streaming via -// the H1/H2 response adapter. -// -// # Cookies -// -// Read and write cookies: -// -// val, err := c.Cookie("session") -// c.SetCookie(&celeris.Cookie{Name: "session", Value: token, HTTPOnly: true}) -// -// # Authentication -// -// Extract HTTP Basic Authentication credentials: -// -// user, pass, ok := c.BasicAuth() -// -// # Request Body Parsing -// -// Bind auto-detects the format from Content-Type: -// -// var user User -// if err := c.Bind(&user); err != nil { -// return err -// } -// -// Or use format-specific methods: -// -// c.BindJSON(&user) // application/json -// c.BindXML(&user) // application/xml -// -// For raw body access: -// -// body := c.Body() // []byte, valid only during handler -// safe := c.BodyCopy() // []byte, safe to retain after handler -// r := c.BodyReader() // io.Reader wrapper -// -// # Response Methods -// -// Context provides typed response methods: -// -// c.JSON(200, data) // application/json -// c.XML(200, data) // application/xml -// c.HTML(200, "

Hello

") // text/html -// c.String(200, "Hello, %s", name) // text/plain (fmt.Sprintf) -// c.Blob(200, "image/png", pngBytes) // arbitrary content type -// c.NoContent(204) // status only, no body -// c.Redirect(302, "/new-location") // redirect with Location header -// c.File("/path/to/report.pdf") // file with MIME detection + Range -// c.FileFromDir(baseDir, userPath) // safe file serving (traversal-safe) -// c.Stream(200, "text/plain", reader) // io.Reader → response (100 MB cap) -// c.Respond(200, data) // auto-format based on Accept header -// -// All response methods return ErrResponseWritten if called after a response -// has already been sent. -// -// # Content Negotiation -// -// Inspect the Accept header and auto-select the response format: -// -// best := c.Negotiate("application/json", "application/xml", "text/plain") -// -// Or use Respond to auto-format based on Accept: -// -// return c.Respond(200, myStruct) // JSON, XML, or text based on Accept -// -// # Accept Negotiation -// -// Beyond content type, negotiate encodings and languages: -// -// enc := c.AcceptsEncodings("gzip", "br", "identity") -// lang := c.AcceptsLanguages("en", "fr", "de") -// -// # Route-Level Middleware -// -// Attach middleware to individual routes without creating a group: -// -// s.GET("/admin", adminHandler).Use(authMiddleware) -// -// Route.Use inserts middleware before the final handler, after server/group -// middleware. Must be called before Server.Start. -// -// # Response Capture -// -// Middleware can inspect the response body after c.Next() by opting in: -// -// func logger() celeris.HandlerFunc { -// return func(c *celeris.Context) error { -// c.CaptureResponse() -// err := c.Next() -// body := c.ResponseBody() // captured response body -// ct := c.ResponseContentType() // captured Content-Type -// // ... log body, ct ... -// return err -// } -// } -// -// # Response Buffering -// -// Middleware that needs to transform response bodies (compress, ETag, cache) -// uses BufferResponse to intercept and modify the response before it is sent: -// -// func compress() celeris.HandlerFunc { -// return func(c *celeris.Context) error { -// c.BufferResponse() -// err := c.Next() -// if err != nil { -// return err -// } -// body := c.ResponseBody() -// compressed := gzip(body) -// c.SetResponseBody(compressed) -// c.SetHeader("content-encoding", "gzip") -// return c.FlushResponse() -// } -// } -// -// BufferResponse is depth-tracked: multiple middleware layers can each call -// BufferResponse, and the response is only sent when the outermost layer -// calls FlushResponse. If middleware forgets to flush, a safety net in the -// handler adapter auto-flushes the response. -// -// CaptureResponse and BufferResponse serve different purposes. CaptureResponse -// is read-only: the response is written to the wire AND a copy is captured for -// inspection (ideal for loggers). BufferResponse defers the wire write entirely, -// allowing middleware to transform the body before sending (ideal for compress, -// ETag, cache). If both are active, BufferResponse takes precedence. -// -// # Response Inspection -// -// Check response state from middleware after calling c.Next(): -// -// written := c.IsWritten() // true after response sent to wire -// size := c.BytesWritten() // response body size in bytes -// status := c.StatusCode() // status code set by handler -// status := c.ResponseStatus() // captured status (with BufferResponse) -// hdrs := c.RequestHeaders() // all request headers as [][2]string -// -// # Error Types -// -// Handlers signal HTTP errors via HTTPError: -// -// return celeris.NewHTTPError(404, "user not found") -// return celeris.NewHTTPError(500, "db error").WithError(err) // wrap cause -// -// Sentinel errors for common conditions: -// -// celeris.ErrResponseWritten // response already sent -// celeris.ErrEmptyBody // Bind called with empty body -// celeris.ErrNoCookie // Cookie() with missing cookie -// celeris.ErrHijackNotSupported // Hijack on unsupported connection -// -// # Flow Control -// -// Middleware calls Next to invoke downstream handlers. Abort stops the chain: -// -// err := c.Next() // call next handler; returns first error -// c.Abort() // stop chain (does not write response) -// c.AbortWithStatus(403) // stop chain and send status code -// c.IsAborted() // true if Abort was called -// -// # Key-Value Storage -// -// Store request-scoped data for sharing between middleware and handlers. -// Three storage surfaces are provided; pick the narrowest one that fits -// the value type: -// -// // Generic any-typed store. Backwards compatible; boxes into interface{}. -// c.Set("userID", 42) -// v, ok := c.Get("userID") -// all := c.Keys() // copy of all stored pairs -// -// // Typed string store — no interface boxing, 1 alloc saved per layer. -// c.SetString("tenantID", "acme-prod") -// s, ok := c.GetString("tenantID") -// -// // Dedicated zero-alloc request-ID field. Same value surfaces via -// // c.Get(celeris.RequestIDKey) for back-compat. -// c.SetRequestID("abcd-1234") -// id := c.RequestID() -// -// GetString falls back to the any-typed c.keys map and to the dedicated -// request-ID field, so code calling c.Get(key) continues to see values -// written via c.SetString. The csrf, basicauth, keyauth, and requestid -// middleware use the typed accessors; custom middleware that carry -// string-only request-scoped values should prefer them too. -// -// # Content-Disposition -// -// Set Content-Disposition headers for file downloads or inline display: -// -// c.Attachment("report.pdf") // prompts download -// c.Inline("image.png") // suggests inline display -// -// # Request Detection -// -// Detect request characteristics: -// -// c.IsWebSocket() // true if Upgrade: websocket -// c.IsTLS() // true if X-Forwarded-Proto is "https" -// -// # Form Presence -// -// FormValueOK distinguishes a missing field from an empty value: -// -// val, ok := c.FormValueOK("name") -// if !ok { -// // field was not submitted -// } -// -// # Request Inspection -// -// Additional request accessors: -// -// scheme := c.Scheme() // "http" or "https" (checks X-Forwarded-Proto) -// ip := c.ClientIP() // from X-Forwarded-For or X-Real-Ip -// method := c.Method() // HTTP method -// path := c.Path() // path without query string -// full := c.FullPath() // matched route pattern (e.g. "/users/:id") -// raw := c.RawQuery() // raw query string without leading '?' -// -// # Remote Address -// -// Context.RemoteAddr returns the TCP peer address. On native engines (epoll, -// io_uring), the address is captured from accept(2) or getpeername(2). On the -// std engine, it comes from http.Request.RemoteAddr. -// -// addr := c.RemoteAddr() // e.g. "192.168.1.1:54321" -// -// # Host -// -// Context.Host returns the request host, checking the :authority pseudo-header -// first (HTTP/2) then falling back to the Host header (HTTP/1.1): -// -// host := c.Host() // e.g. "example.com" -// -// # Connection Hijacking -// -// HTTP/1.1 connections can be hijacked on all engines for WebSocket or -// other protocols that require raw TCP access: -// -// conn, err := c.Hijack() -// if err != nil { -// return err -// } -// defer conn.Close() -// // conn is a net.Conn — caller owns the connection -// -// HTTP/2 connections cannot be hijacked because multiplexed streams -// share a single TCP connection. -// -// # Graceful Restart -// -// Start the server with a pre-existing listener for zero-downtime deploys: -// -// ln, _ := celeris.InheritListener("LISTEN_FD") -// if ln != nil { -// log.Fatal(s.StartWithListener(ln)) -// } else { -// log.Fatal(s.Start()) -// } -// -// # Config Surface Area -// -// In addition to the basic config fields, the following tuning fields -// are available: -// -// celeris.Config{ -// // Basic -// Addr: ":8080", -// Protocol: celeris.Auto, // HTTP1, H2C, or Auto -// Engine: celeris.Adaptive, // IOUring, Epoll, Adaptive, Std -// -// // Workers -// Workers: 0, // I/O workers (default GOMAXPROCS) -// -// // Timeouts (0 = default: 300s read/write, 600s idle; -1 = no timeout) -// ReadTimeout: 0, -// WriteTimeout: 0, -// IdleTimeout: 0, -// ShutdownTimeout: 30*time.Second, -// -// // Limits -// MaxFormSize: 0, // multipart form memory (0 = 32 MB; -1 = unlimited) -// MaxConns: 0, // max connections per worker (0 = unlimited) -// -// // H2 Tuning -// MaxConcurrentStreams: 100, // H2 streams per connection -// MaxFrameSize: 16384, // H2 frame payload size -// InitialWindowSize: 65535, // H2 stream flow-control window -// MaxHeaderBytes: 0, // header block size (0 = 16 MB) -// -// // I/O -// DisableKeepAlive: false, // disable HTTP keep-alive -// BufferSize: 0, // per-connection I/O buffer (0 = engine default) -// SocketRecvBuf: 0, // SO_RCVBUF (0 = OS default) -// SocketSendBuf: 0, // SO_SNDBUF (0 = OS default) -// -// // Observability -// DisableMetrics: false, // skip per-request metric recording -// Logger: nil, // *slog.Logger for engine diagnostics -// -// // Connection callbacks (must be fast — blocks the event loop) -// OnConnect: func(addr string) { ... }, -// OnDisconnect: func(addr string) { ... }, -// } -// -// # Listener Address -// -// After Start or StartWithContext, Server.Addr returns the bound address. -// This is useful when listening on ":0" to discover the OS-assigned port: -// -// addr := s.Addr() // net.Addr; use addr.String() for "127.0.0.1:49152" -// -// # Testing -// -// The github.com/goceleris/celeris/celeristest package provides test helpers: -// -// ctx, rec := celeristest.NewContext("GET", "/hello") -// defer celeristest.ReleaseContext(ctx) -// handler(ctx) -// // inspect rec.StatusCode, rec.Headers, rec.Body +// Full guides, tutorials and examples: https://goceleris.dev/docs +// Start here: https://goceleris.dev/docs/getting-started package celeris diff --git a/driver/internal/async/doc.go b/driver/internal/async/doc.go index 0c13ed81..471dd93a 100644 --- a/driver/internal/async/doc.go +++ b/driver/internal/async/doc.go @@ -21,7 +21,7 @@ // - [Backoff]: exponential-with-jitter delay computation for retry // loops. Not safe for concurrent use. // -// - Internal [health] checker scheduled by the pool. +// - Internal health checker scheduled by the pool. // // Drivers build their user-facing API (postgres.Pool, redis.Client) by // composing these primitives with a protocol-specific dispatch loop. diff --git a/driver/memcached/doc.go b/driver/memcached/doc.go index a18f1f92..35dae85f 100644 --- a/driver/memcached/doc.go +++ b/driver/memcached/doc.go @@ -1,217 +1,43 @@ -// Package memcached is celeris's native Memcached driver. It speaks both the -// text and binary wire protocols directly on top of the celeris event loop, -// using the same async-bridge architecture as the Redis and PostgreSQL -// drivers: one in-flight request enqueued per conn, responses demuxed from -// the loop's recv callback, and per-worker idle pools so handlers stay on -// the same CPU that dispatched them. -// -// # Usage -// -// client, err := memcached.NewClient("localhost:11211", -// memcached.WithEngine(srv), // optional; omit for a standalone loop -// memcached.WithProtocol(memcached.ProtocolText), -// ) +// Package memcached is celeris's native Memcached driver, speaking the text +// and binary wire protocols directly on the celeris event loop. +// +// [NewClient] returns a [Client], a pooled single-node handle whose typed API +// covers storage (Set, Add, Replace, Append, Prepend, CAS and their *Bytes +// variants), retrieval (Get, GetBytes, GetMulti, GetMultiBytes, Gets for the +// CAS token), arithmetic (Incr, Decr), key ops (Delete, Touch), and server +// ops (Flush, FlushAfter, Stats, Version, Ping). [ClusterClient], built with +// [NewClusterClient], mirrors the same command surface but shards keys across +// several nodes via a libmemcached-compatible ketama ring with passive failure +// detection and background health probing. +// +// Behavior is tuned with [Option] values passed to [NewClient]: [WithProtocol] +// ([ProtocolText] default, [ProtocolBinary] opt-in), [WithEngine] to colocate +// socket I/O on a celeris.Server's worker threads, [WithMaxOpen], +// [WithMaxIdlePerWorker], [WithTimeout], [WithDialTimeout], [WithMaxLifetime], +// [WithMaxIdleTime], and [WithHealthCheckInterval]. Addresses may carry an +// optional "memcache://" or "memcached://" scheme prefix. +// +// Misses and precondition failures surface as the sentinels [ErrCacheMiss], +// [ErrNotStored], and [ErrCASConflict]; other client-side faults as +// [ErrClosed], [ErrProtocol], [ErrPoolExhausted], [ErrMalformedKey], +// [ErrInvalidCAS], and (for clusters) [ErrNoNodes]. Other server-side replies arrive as [*MemcachedError]. +// +// TLS, SASL authentication, and server-side compression are not supported; +// deploy over a trusted network and compress large values client-side. +// [ClusterClient] topology is static — to add or remove nodes, rebuild the +// client or front the fleet with a proxy such as mcrouter. +// +// client, err := memcached.NewClient("localhost:11211") +// if err != nil { +// log.Fatal(err) +// } // defer client.Close() -// // if err := client.Set(ctx, "key", "value", time.Minute); err != nil { // log.Fatal(err) // } // v, err := client.Get(ctx, "key") // -// [WithEngine] routes the driver's FDs through the same epoll/io_uring -// instance as the HTTP workers. When it is omitted, a package-level -// standalone loop is resolved and reference-counted -// (driver/internal/eventloop). -// -// # DSNs -// -// Addresses may optionally include a "memcache://" or "memcached://" scheme -// prefix; the prefix is stripped. The resulting "host:port" is passed to -// net.Dialer verbatim. TLS is not supported in v1.4.0. -// -// # Options -// -// - [WithProtocol] selects the wire dialect (text or binary); default text. -// - [WithMaxOpen] caps the total number of connections across workers. -// - [WithMaxIdlePerWorker] bounds per-worker idle pool size. -// - [WithTimeout] sets an advisory per-op deadline when ctx carries none. -// - [WithDialTimeout] sets the TCP dial timeout. -// - [WithMaxLifetime] / [WithMaxIdleTime] bound pooled-conn age and idleness. -// - [WithHealthCheckInterval] configures the background health sweep. -// - [WithEngine] wires the driver into a celeris.Server's event loop. -// -// # Typed Commands -// -// The typed API on [Client] covers: -// -// - Storage: Set, Add, Replace, Append, Prepend, CAS. -// - Retrieval: Get, GetBytes, GetMulti, GetMultiBytes, Gets (returns CAS). -// - Arithmetic: Incr, Decr. -// - Keys: Delete, Touch. -// - Server: Flush, FlushAfter, Stats, Version, Ping. -// -// Values passed to Set / Add / Replace / CAS may be string, []byte, integer, -// float, bool, nil, or fmt.Stringer. Anything else returns an error. -// -// # Errors -// -// - [ErrCacheMiss] — key not found. -// - [ErrNotStored] — Add/Replace/Append/Prepend precondition failed. -// - [ErrCASConflict] — CAS token mismatch (or concurrent modification). -// - [ErrClosed] — command issued against a closed client. -// - [ErrProtocol] — reply did not parse. -// - [ErrMalformedKey] — key violates text-protocol constraints (whitespace, -// control bytes, > 250 bytes). -// - [ErrPoolExhausted] — all idle conns are stale (rare; pool blocks when full). -// -// Non-sentinel server errors surface as [*MemcachedError] carrying the reply -// kind ("ERROR", "CLIENT_ERROR", "SERVER_ERROR") or, in binary mode, the -// raw status code. -// -// # Pool -// -// Like the Redis and Postgres drivers, memcached uses [async.Pool] for -// worker-affinity connection pooling. Each worker owns an idle list guarded -// by its own lock; Acquire prefers the caller's worker before scanning -// neighbors. Acquire blocks when MaxOpen is reached (wait-queue semantics) -// rather than returning ErrPoolExhausted. -// -// # WithEngine and standalone operation -// -// [WithEngine] is optional. When omitted, [NewClient] resolves a standalone -// event loop backed by the platform's best mechanism (epoll on Linux, -// goroutine-per-conn on Darwin). The standalone loop is reference-counted -// inside driver/internal/eventloop and shared across all drivers that omit -// WithEngine. Correctness is identical with or without WithEngine; sharing -// the HTTP server's event loop reduces per-IO syscalls and improves data -// locality because driver FDs land on the same worker goroutine as HTTP -// handlers. -// -// # Text vs binary protocol -// -// The text protocol is the default. Every memcached deployment supports it, -// responses are self-describing, and the parser is trivially debugged by -// reading a PCAP. The binary protocol is opt-in via [WithProtocol]; it adds -// an opaque correlation ID, fixed-size framing, and explicit status codes. -// The binary dialect is useful where malformed user input or embedded NULs -// in values must round-trip cleanly. Both dialects truly pipeline -// [Client.GetMulti] in a single round trip — binary uses OpGetKQ + OpNoop, -// text uses the multi-key `get` command. Multi-packet server replies such -// as binary Stats are decoded natively on the active conn. -// -// # Expiration times -// -// Callers pass a Go time.Duration relative TTL. The driver rewrites it on -// the wire as either a relative second-count (when <= 30 days) or an -// absolute Unix timestamp (when > 30 days), matching the memcached server -// convention. Pass 0 for no expiration. -// -// # Key validation -// -// Keys are validated against text-protocol rules (1..250 bytes, no -// whitespace or control bytes) uniformly in both dialects, so that callers -// who switch from text to binary do not accidentally ship previously -// valid-looking keys that the text server would have rejected. -// -// # Multi-server sharding (ClusterClient) -// -// Production memcached deployments fan keys across N independent nodes; the -// memcached protocol has no cluster-aware peer discovery, so sharding is a -// client-side concern. [ClusterClient] owns a static ring of [Client] -// instances (one per configured address) and routes each key via a -// libmemcached-compatible ketama consistent-hash ring. -// -// cc, err := memcached.NewClusterClient(memcached.ClusterConfig{ -// Addrs: []string{ -// "memcache-a:11211", -// "memcache-b:11211", -// "memcache-c:11211", -// }, -// // Optional: relative weights (nil = equal). -// // Weights: []uint32{1, 2, 1}, -// }) -// defer cc.Close() -// cc.Set(ctx, "user:42", "payload", time.Hour) -// -// The ring assigns 160 virtual nodes per unit weight, placed at -// MD5("-") boundaries (4 ring points per digest). Lookup uses -// CRC32-IEEE on the user key and a binary search with wrap-around. Removing -// one of N nodes only re-homes the ~1/N share of keys it owned; the rest -// keep their owner, matching the usual ketama guarantee. -// -// The [ClusterClient] API mirrors [Client] command-for-command: -// -// - Single-key ops (Get, Set, Delete, Incr, ...) route to pickNode(key). -// - GetMulti / GetMultiBytes partition keys by owner, fan out one -// sub-request per node in parallel, and merge the per-node responses. -// On error the first error wins; partial results from other nodes are -// discarded. -// - Stats, Version return a per-node map keyed by address. -// - Flush, FlushAfter, Ping fan out to every node; the first error wins. -// - NodeFor(key) exposes the ring's routing decision for debugging and -// for out-of-band work that needs to track per-node state. -// -// Topology is static for the lifetime of the client: memcached nodes do -// not gossip and [ClusterClient] does not run a background refresh loop. -// Adding or removing a node requires tearing down the client and building -// a new one. For a dynamically-changing fleet, deploy a proxy such as -// mcrouter or twemproxy in front of the nodes and point the single-node -// [Client] at the proxy. -// -// # Node failure detection and failover (v1.4.1) -// -// [ClusterClient] tracks the health of each node and automatically -// reroutes around failed ones so a single dead node does not translate -// into per-key errors for every key in its slice of the ring. -// -// Detection is passive + probe: -// -// - Each [Client] operation issued through the cluster updates the -// node's consecutive-failure counter. Infrastructure errors (dial -// failures, I/O errors, protocol corruption) count; protocol-level -// responses ([ErrCacheMiss], [ErrNotStored], [ErrCASConflict], -// [*MemcachedError]) do NOT — they are valid server replies. -// - Once the counter reaches [ClusterConfig.FailureThreshold] (default -// 2), the node is marked failing. [ClusterClient.pickNode] then -// walks the node list clockwise from the failing node's position -// and returns the first healthy neighbor it finds. -// - A background goroutine (configurable via -// [ClusterConfig.HealthProbeInterval], default 5s) pings any -// failing node that has been down for at least 1 second and clears -// the failing flag on success. -// - Any successful operation passively clears the flag — so a node -// that silently recovers while still receiving traffic from -// passed-through requests is picked up immediately. -// -// Key-redistribution implications: -// -// - The successor is the next node in insertion order (the order -// given in [ClusterConfig.Addrs]), NOT the next ring neighbor. -// This preserves the consistent-hash invariant that all keys -// formerly routed to failing node N now route to the same -// successor — rather than being scattered across the ring. -// - During the failure transition, keys that were cached on N will -// MISS on the successor. During recovery, keys that were cached on -// the successor will MISS on N. Applications must tolerate both -// transitions — caches are not authoritative. -// -// Observability: [ClusterClient.NodeHealth] returns a snapshot of the -// failing flag per address; [ClusterClient.NodeStatsMap] includes the -// last-fail timestamp and consecutive-fail counter. -// -// Semantics vs. gomemcache/dalli: celeris matches dalli's "mark after -// N failures, passive heal on success" policy. The background probe -// is an addition for deployments where a failing node receives no -// traffic (otherwise the passive path would never clear the flag). -// -// # Known limitations (v1.4.0) +// # Documentation // -// - No TLS — deploy over VPC, loopback, or a sidecar. -// - No SASL authentication; memcached servers configured with -Y will -// reject the driver's handshake. Deploy with network-level auth. -// - No server-side compression. Values are sent as-is; large payloads -// should be compressed on the client side. -// - [ClusterClient] topology is static: no background refresh. Memcached -// nodes do not gossip. To add/remove nodes at runtime, tear down and -// rebuild the client, or front the fleet with mcrouter. +// Full guides and examples: https://goceleris.dev/docs/data-stores package memcached diff --git a/driver/postgres/doc.go b/driver/postgres/doc.go index a2baa8ad..6730d397 100644 --- a/driver/postgres/doc.go +++ b/driver/postgres/doc.go @@ -1,226 +1,46 @@ // Package postgres is celeris's native PostgreSQL driver. It speaks the // PostgreSQL v3 wire protocol directly on top of the celeris event loop, -// exposes a database/sql compatible driver, and also provides a lower-level -// worker-affinity Pool for callers that want to skip database/sql overhead. +// exposes a database/sql-compatible driver, and also provides a lower-level +// worker-affinity [Pool] for callers that want to skip database/sql overhead. // -// # Usage +// There are two entry points. Use database/sql for portability (works with +// any ORM); use the direct [Pool] for peak throughput. // -// Two entry points are supported. Which one to pick depends on whether the -// caller wants portability (database/sql + any ORM) or peak throughput -// (direct Pool bound to a celeris.Server). -// -// (a) database/sql -// -// import ( -// "database/sql" -// _ "github.com/goceleris/celeris/driver/postgres" -// ) -// -// db, err := sql.Open("celeris-postgres", "postgres://app:pass@localhost/mydb?sslmode=disable") -// -// database/sql owns the pool in this mode. The driver registers itself under -// [DriverName] in its init. Every *sql.Conn handed out by db is a pgConn -// running on a standalone event loop (one is resolved and reference-counted -// inside driver/internal/eventloop). -// -// (b) Direct Pool +// // database/sql — registers itself under DriverName in init. +// import _ "github.com/goceleris/celeris/driver/postgres" +// db, err := sql.Open(postgres.DriverName, "postgres://app:pass@localhost/mydb?sslmode=disable") // +// // Direct Pool — optionally bound to a celeris.Server event loop. // pool, err := postgres.Open(dsn, postgres.WithEngine(srv)) -// defer pool.Close() -// -// rows, err := pool.QueryContext(ctx, "SELECT id, name FROM users WHERE tenant = $1", tenantID) -// -// The direct Pool pins each connection to a worker (see [WithWorker] for the -// context-based hint API), so a handler running on worker N preferentially -// re-uses conns whose event-loop callbacks also land on worker N. When -// [WithEngine] is passed, the Pool shares the same epoll/io_uring instance -// as the HTTP workers; otherwise a standalone driver loop is resolved. -// -// # DSNs -// -// Both the libpq URL form and the key=value form are accepted. -// -// URL form: -// -// postgres://user:pass@host:5432/dbname?sslmode=disable&application_name=svc&connect_timeout=5 -// -// Key=value form: -// -// host=localhost port=5432 user=app password=secret dbname=mydb sslmode=disable -// -// Recognized DSN keys: host, port, user, password, dbname / database, sslmode, -// connect_timeout (seconds), statement_cache_size, auto_cache_statements, -// application_name. Any other key is forwarded to the server as a -// StartupMessage parameter (so search_path, timezone, statement_timeout, and -// similar GUCs can be set at connect time). -// -// auto_cache_statements (default true at the [Pool.Open] / [NewConnector] -// layer): when true, cacheable SELECT-style QueryContext calls with -// arguments transparently auto-prepare on first use and reuse the -// prepared statement (Bind+Execute+Sync) on subsequent invocations. -// Mirrors pgx's QueryExecModeCacheStatement default. Set -// `auto_cache_statements=false` in the DSN to opt out and stay on the -// extended-protocol-without-cache path. Arg-less queries always take the -// simple-query path regardless. -// -// # Options -// -// Pool knobs are supplied as functional options to [Open]: -// -// - [WithEngine] bind the pool to a celeris.Server event loop. -// - [WithMaxOpen] total conn cap (default NumWorkers*4). -// - [WithMaxIdlePerWorker] per-worker idle list cap (default 2). -// - [WithMaxLifetime] max conn age (default 30m). -// - [WithMaxIdleTime] max idle duration (default 5m). -// - [WithHealthCheck] background sweep interval (default 30s; 0 disables). -// - [WithStatementCacheSize] per-conn prepared-statement LRU (default 256). -// - [WithApplication] application_name startup parameter. -// -// # Transactions -// -// [Pool.BeginTx] opens a [Tx] that pins its connection until [Tx.Commit] or -// [Tx.Rollback] is called. Passing *sql.TxOptions selects the isolation level -// (Read Uncommitted / Read Committed / Repeatable Read / Serializable) and -// the read-only flag, all folded into a single BEGIN round trip. -// -// Savepoints are reachable three ways: -// -// 1. (*postgres.Tx).Savepoint / ReleaseSavepoint / RollbackToSavepoint on -// the direct Pool transaction. -// -// 2. database/sql via sql.Conn.Raw + the exported [Conn] alias: -// -// conn, _ := db.Conn(ctx) -// defer conn.Close() -// _ = conn.Raw(func(raw any) error { -// pc := raw.(*postgres.Conn) -// return pc.Savepoint(ctx, "sp1") -// }) -// -// 3. Raw simple queries ("SAVEPOINT sp1") issued through Exec on a -// transaction; the first two forms are preferred because they -// validate the name. -// -// Savepoint names must match [A-Za-z0-9_]+; other identifiers are -// rejected before the wire write to avoid SQL-injection. -// -// # Bulk COPY -// -// [Pool.CopyFrom] streams rows into a table via COPY FROM STDIN. Callers -// implement [CopyFromSource] (or use [CopyFromSlice] for in-memory fixtures) -// to feed rows. [Pool.CopyTo] runs COPY ... TO STDOUT and invokes a callback -// for each raw row. Both use PG's text format with tab separators and -// backslash escaping. -// -// # Pool configurations -// -// database/sql and postgres.Pool each maintain their own set of connections. -// Opening a sql.DB with sql.Open("celeris-postgres", dsn) and separately -// calling postgres.Open(dsn) does not share pool state — configure -// sql.DB.SetMaxOpenConns on the former and [WithMaxOpen] on the latter -// independently. -// -// # Type support -// -// The encoder/decoder understands the following server OIDs out of the box -// (see postgres/protocol for the full codec table): -// -// bool, int2, int4, int8, float4, float8, text, varchar, bytea, uuid, -// date, timestamp, timestamptz, numeric, json, jsonb, and the one- -// dimensional array variants of those types (_bool, _int4, _text, ...). -// -// Go type mappings: -// -// bool ↔ bool -// int2/int4/int8 ↔ int64 (accepts int, int32 at encode) -// float4/float8 ↔ float64 -// text/varchar ↔ string -// bytea ↔ []byte -// uuid ↔ []byte (16 bytes) -// date/timestamp/tstz ↔ time.Time -// numeric ↔ string (use strconv or math/big to convert) -// json/jsonb ↔ []byte or string -// arrays ↔ []T of the element's Go type -// -// Any argument that implements [database/sql/driver.Valuer] is resolved to -// its underlying value before encoding. Any destination that implements -// [database/sql.Scanner] receives the raw bytes. Custom types can be plugged -// in via [protocol.RegisterType]. -// -// # Errors -// -// Server-side errors surface as [*PGError] (a re-export of -// [protocol.PGError]). The struct carries the five-character SQLSTATE code, -// the short message, and any optional fields the server attached (detail, -// hint, constraint name, etc.). Sentinels are wrapped so errors.Is and -// errors.As work as expected: -// -// var pgErr *postgres.PGError -// if errors.As(err, &pgErr) && pgErr.Code == "23505" { ... } -// -// Other package-level sentinels: [ErrPoolClosed], [ErrClosed], [ErrBadConn], -// [ErrSSLNotSupported], [ErrUnsupportedAuth]. -// -// # Query cancellation -// -// Canceling a context aborts the in-flight query by dialing a short-lived -// side connection and sending the PostgreSQL v3 CancelRequest packet -// (protocol code 80877102). The client blocks until the server drains the -// rest of the original query's response — cancellation is cooperative, not -// instantaneous. -// -// # WithEngine and standalone operation -// -// [WithEngine] is optional. When omitted, [Open] resolves a standalone event -// loop backed by the platform's best mechanism (epoll on Linux, goroutine- -// per-conn on Darwin). The standalone loop is reference-counted inside -// driver/internal/eventloop and shared across all drivers that omit WithEngine. -// Correctness is identical with or without WithEngine; the difference is -// performance: sharing the HTTP server's event loop improves data locality -// and saves one epoll/uring syscall per I/O. Expect ~5-20% lower latency for -// serial queries when WithEngine is used. -// -// database/sql mode (sql.Open) always uses the standalone loop. For optimal -// performance, prefer the direct Pool with WithEngine(srv). -// -// # Rows iteration -// -// [Pool.QueryContext] returns a *Rows value. Iterate with Next + Scan: -// -// rows, err := pool.QueryContext(ctx, "SELECT id, name FROM users") -// if err != nil { return err } -// defer rows.Close() -// for rows.Next() { -// var id int64 -// var name string -// if err := rows.Scan(&id, &name); err != nil { return err } -// // process id, name -// } -// if err := rows.Err(); err != nil { -// return err // check Err() after the loop -// } -// -// Always check [Rows.Err] after the loop — it surfaces errors from the -// underlying query that were deferred until iteration completed (e.g. -// cancellation, network errors, or server-side errors on large result sets). -// -// # Known limitations // -// - TLS is not yet supported. sslmode=require, verify-ca, and verify-full -// are rejected at [Open] time with [ErrSSLNotSupported]. Deploy over -// VPC, loopback, or a sidecar TLS terminator. -// - Result sets are fully buffered before Rows.Next returns — there is no -// true row-by-row streaming. Callers with large result sets should page -// via LIMIT/OFFSET or a server-side DECLARE CURSOR inside a transaction. -// - Authentication supports trust, cleartext, MD5, and SCRAM-SHA-256 -// (without channel binding). GSS, SSPI, Kerberos, and SCRAM-SHA-256-PLUS -// are not implemented. -// - No LISTEN / NOTIFY. Asynchronous NotificationResponse frames received -// from the server are silently dropped by the dispatcher. -// - COPY IN / COPY OUT is exposed via [Pool.CopyFrom] / [Pool.CopyTo] on -// the direct Pool API. database/sql has no analogous surface; callers -// who need COPY must use the direct Pool. -// - No PG 14+ pipeline mode (Execute chaining). -// - No cluster- or replica-routing logic; callers with a read replica -// should open a second Pool pointed at it. +// [Open] accepts both the libpq URL form and the key=value DSN form. Pool +// behavior is tuned with functional [Option] values: [WithEngine], +// [WithMaxOpen], [WithMaxIdlePerWorker], [WithMaxLifetime], [WithMaxIdleTime], +// [WithHealthCheck], [WithStatementCacheSize], and [WithApplication]. +// [WithEngine] is optional and affects performance only: when set, the pool +// shares the HTTP server's event loop instead of resolving a standalone one. +// [WithWorker] adds a per-call worker-affinity hint to a context. +// +// Query with [Pool.QueryContext] (returns [*Rows]), [Pool.QueryRow] +// (returns [*Row]), or [Pool.ExecContext] (returns [Result]). Transactions +// come from [Pool.BeginTx], which returns a [*Tx] with [Tx.Commit], +// [Tx.Rollback], and savepoint helpers ([Tx.Savepoint], +// [Tx.ReleaseSavepoint], [Tx.RollbackToSavepoint]). Bulk loads use +// [Pool.CopyFrom] (feed rows via [CopyFromSource] or [CopyFromSlice]) and +// [Pool.CopyTo]. +// +// Server errors surface as [*PGError] (alias of [protocol.PGError]) carrying +// the SQLSTATE code; match with errors.As. Package sentinels include +// [ErrPoolClosed], [ErrClosed], [ErrBadConn], [ErrSSLNotSupported], +// [ErrUnsupportedAuth], and [ErrResultTooBig]. Custom type codecs can be +// registered with [protocol.RegisterType]. +// +// Current limitations: TLS is not yet supported (use sslmode=disable behind a +// VPC/loopback/sidecar); authentication is limited to trust, cleartext, MD5, +// and SCRAM-SHA-256 without channel binding; there is no LISTEN/NOTIFY, +// pipeline mode, or replica routing. +// +// # Documentation +// +// Full guides and examples: https://goceleris.dev/docs/data-stores package postgres diff --git a/driver/redis/doc.go b/driver/redis/doc.go index 830470b1..c98ff181 100644 --- a/driver/redis/doc.go +++ b/driver/redis/doc.go @@ -1,264 +1,42 @@ -// Package redis is celeris's native Redis driver. It speaks RESP2 and RESP3 -// directly on top of the celeris event loop using the same async-bridge -// architecture as the celeris PostgreSQL driver: one in-flight request -// enqueued per conn, responses demuxed from the loop's recv callback, and -// per-worker idle pools so handlers stay on the same CPU that dispatched -// them. -// -// # Usage -// -// client, err := redis.NewClient("localhost:6379", -// redis.WithEngine(srv), // optional; omit for a standalone loop -// redis.WithPassword("secret"), -// redis.WithDB(0), -// ) +// Package redis is celeris's native RESP2/RESP3 Redis driver, designed to run +// its socket I/O on the celeris event loop alongside your HTTP handlers. +// +// [NewClient] returns a [Client] backed by a per-worker connection pool; +// connections are dialed lazily on first command and negotiate RESP3 (HELLO 3) +// with automatic RESP2 fallback. Pass [WithEngine] to colocate the driver's +// file descriptors on the same workers as a running celeris server (lower +// latency via better data locality); omit it to use a standalone, internally +// reference-counted event loop. Other options include [WithPassword], +// [WithUsername], [WithDB], [WithForceRESP2], [WithProto], and [WithOnPush]; +// see [Config] for the full set. +// +// client, err := redis.NewClient("localhost:6379", redis.WithEngine(srv)) // defer client.Close() -// // v, err := client.Get(ctx, "key") // -// [WithEngine] routes the driver's FDs through the same epoll/io_uring -// instance as the HTTP workers. When it is omitted, a package-level -// standalone loop is resolved and reference-counted -// (driver/internal/eventloop). -// -// Connections are lazily dialed on first command. The first dial does the -// RESP3 handshake (HELLO 3 + AUTH + SETNAME), falls back to RESP2 + AUTH + -// SELECT if HELLO is rejected, or speaks RESP2 unconditionally when -// [WithForceRESP2] is set (required for ElastiCache classic clusters and -// for older servers that do not implement HELLO). -// -// # Commands -// -// The typed API on [Client] covers: -// -// - Strings: Get, GetBytes, Set, SetNX, Del, Exists, Incr, Decr, MGet, -// Expire, PExpire, ExpireAt, PExpireAt, Persist, TTL. -// - Hashes: HGet, HSet, HDel, HGetAll, HExists, HKeys, HVals. -// - Lists: LPush, RPush, LPop, RPop, LRange, LLen. -// - Sets: SAdd, SRem, SMembers, SIsMember, SCard. -// - Sorted sets: ZAdd, ZRange, ZRangeByScore, ZRem, ZScore, ZCard. -// - Keys: Type, Rename, RandomKey, Scan (cursor iteration). -// - Pub/Sub: Publish, Subscribe, PSubscribe (see below). -// - Scripting: Eval, EvalSHA, ScriptLoad. -// - Watch for optimistic locking (pins a conn for the callback). -// -// Any command the typed surface does not expose can be sent via [Client.Do], -// which accepts ...any args, converts them through [argify], and returns a -// [*protocol.Value] plus a server-side error as [*RedisError]. -// -// # Pipeline -// -// [Client.Pipeline] batches commands into a single network write. Each -// queued call returns a typed handle (*StringCmd, *IntCmd, ...); the -// caller then invokes [Pipeline.Exec] to flush and harvest the replies: -// -// p := client.Pipeline() -// a := p.Set("k", "v", 0) -// b := p.Incr("counter") -// if err := p.Exec(ctx); err != nil { ... } -// _, _ = a.Result() -// n, _ := b.Result() -// -// All commands in one Exec ride the same connection, so replies are -// returned in the same order as commands were enqueued. Replies are -// detached from the reader's scratch buffer before the conn is released, -// so each Result() call is safe to retain. -// -// # Transactions -// -// Typed MULTI/EXEC transactions are built via [Client.TxPipeline]. Queued -// commands return deferred *Cmd handles that populate after [Tx.Exec]: -// -// tx, _ := client.TxPipeline(ctx) -// defer tx.Discard() // no-op after a successful Exec -// a := tx.Incr("visits") -// b := tx.Incr("uniques") -// if err := tx.Exec(ctx); err != nil { ... } -// va, _ := a.Result() -// vb, _ := b.Result() -// -// Exec sends MULTI + the buffered commands + EXEC in a single write. If a -// WATCHed key changed (EXEC returns a null array) every queued *Cmd.Result() -// returns [ErrTxAborted]. WATCH / UNWATCH are available on the Tx itself -// and must be called before any command is queued. -// -// Raw MULTI/EXEC via [Client.Do] also works; the pool tracks the -// MULTI/EXEC/DISCARD state on the pinned conn so a premature release issues -// a DISCARD before the conn returns to the idle list. -// -// # Pub/Sub -// -// [Client.Subscribe] and [Client.PSubscribe] open a dedicated subscriber -// connection. Messages arrive on [PubSub.Channel]: -// -// ps, _ := client.Subscribe(ctx, "events") -// defer ps.Close() -// for m := range ps.Channel() { handle(m) } -// -// On transport error the driver automatically reconnects with exponential -// backoff and replays the tracked SUBSCRIBE/PSUBSCRIBE list. Messages that -// arrive while the connection is down are lost — delivery is at-most-once. -// The channel buffer defaults to 256; when it fills, [PubSub.deliver] drops -// the oldest message to make room and bumps [PubSub.Drops]. -// -// # RESP2 vs RESP3 -// -// RESP3 is negotiated with HELLO 3 during the handshake and brings richer -// reply types (Map, Set, Double, Bool, Verbatim, BigNumber, Push) that the -// driver surfaces via the [protocol.Value] type-tag enum. When the server -// rejects HELLO the driver transparently downgrades to RESP2 and speaks -// AUTH + SELECT, so almost all callers can leave the default alone. -// [WithForceRESP2] pins the connection to RESP2 for deployments that must -// avoid HELLO entirely. [Client.Proto] reports the negotiated version. -// -// # Zero-copy semantics -// -// The RESP reader returns [protocol.Value] structs whose Str / Array / Map -// fields alias the reader's internal buffer. To keep hot-path reads -// allocation-free, the driver does NOT copy on every reply. Instead: -// -// - Typed accessors on [Client] (Get, HGet, MGet, ...) return freshly -// allocated Go strings / slices / maps — the copy happens inside the -// decode helper, so the caller never sees aliased bytes. -// - Pipeline results are detached (deep-copied) before the conn returns -// to the idle pool, so each *Cmd.Result() is independent. -// - [Client.Do] returns a [*protocol.Value] that has already been detached -// from the reader buffer; callers can retain it freely. -// -// The only place raw aliasing is observable is inside custom dispatch -// routines that reach into the internal protocol APIs directly — typed -// callers are always safe. -// -// # Errors -// -// Server-side error replies surface as [*RedisError] with Prefix (e.g. -// "WRONGTYPE", "ERR", "MOVED") and full Msg. Sentinels are wired through -// [RedisError.Is]: -// -// - [ErrNil] null bulk reply (GET on missing key, LPOP on empty list). -// - [ErrClosed] Client or PubSub after Close. -// - [ErrProtocol] reply did not parse. -// - [ErrMoved] cluster redirect (not followed — cluster is unsupported). -// - [ErrWrongType] operation on a key of the wrong kind. -// - [ErrPoolExhausted] all idle conns are stale (rare; pool blocks when full). -// - [ErrTxAborted] WATCH / EXEC aborted the transaction. -// -// # WithEngine and standalone operation -// -// [WithEngine] is optional. When omitted, [NewClient] resolves a standalone -// event loop backed by the platform's best mechanism (epoll on Linux, goroutine- -// per-conn on Darwin). The standalone loop is reference-counted inside -// driver/internal/eventloop and shared across all drivers that omit WithEngine. -// Correctness is identical with or without WithEngine; the difference is -// performance: sharing the HTTP server's event loop saves one epoll/uring -// syscall per I/O because driver FDs land on the same worker goroutine as HTTP -// handlers, improving data locality. Expect ~5-20% lower latency for serial -// queries when WithEngine is used. -// -// # Pipeline.Release lifetime -// -// [Pipeline.Release] returns the Pipeline — and all of its internal slabs — -// to a sync.Pool for reuse. After Release, every typed Cmd handle (*StringCmd, -// *IntCmd, *StatusCmd, *FloatCmd, *BoolCmd) previously returned from the -// Pipeline is invalid. Calling Result() on an invalidated handle returns -// [ErrClosed]. Release is optional; un-Released Pipelines are GC'd normally. -// For tight hot paths, pooling via Release eliminates per-request slab allocs. -// -// # SCAN usage -// -// Cursor-based iteration is exposed via [Client.Scan]: -// -// it := client.Scan(ctx, "user:*", 100) -// for { -// key, ok := it.Next(ctx) -// if !ok { -// break -// } -// // process key -// } -// if err := it.Err(); err != nil { -// log.Fatal(err) -// } -// -// The ScanIterator handles cursor paging internally. The match pattern is -// passed as MATCH and the count hint as COUNT. Both are optional — pass "" -// and 0 to iterate all keys. The iterator is NOT safe for concurrent use. -// -// # Watch (optimistic locking) -// -// [Client.Watch] pins a connection, WATCHes the given keys, and invokes fn -// with a [Tx] that queues commands under MULTI/EXEC. If a WATCHed key is -// modified before EXEC, the transaction aborts with [ErrTxAborted] and the -// caller can retry: -// -// err := client.Watch(ctx, func(tx *redis.Tx) error { -// val, err := client.Get(ctx, "counter") -// if err != nil { return err } -// n, _ := strconv.Atoi(val) -// tx.Set("counter", strconv.Itoa(n+1), 0) -// return tx.Exec(ctx) -// }, "counter") -// -// # Push callbacks (client tracking) -// -// RESP3 push frames on command connections (e.g. CLIENT TRACKING invalidation -// messages) can be intercepted via [WithOnPush] or [Client.OnPush]: -// -// client, _ := redis.NewClient("localhost:6379", -// redis.WithOnPush(func(channel string, data []protocol.Value) { -// log.Printf("push: %s %v", channel, data) -// }), -// ) -// -// When no callback is registered, push frames on command connections are -// silently dropped. Push frames on pub/sub connections are always routed -// through the [PubSub.Channel] mechanism. -// -// # Cluster Transactions -// -// Redis Cluster requires all keys in a MULTI/EXEC transaction to reside in -// the same hash slot. Cross-slot transactions are impossible by design — -// the server returns -CROSSSLOT. The celeris driver validates slot affinity -// client-side and returns [ErrCrossSlot] before contacting the server when -// keys span multiple slots. -// -// Use hash tags to colocate related keys on the same slot: -// -// user:{123}:name → hashes only "{123}" → slot X -// user:{123}:email → hashes only "{123}" → slot X -// -// [ClusterClient.TxPipeline] returns a [ClusterTx] that queues commands and -// verifies all keys target the same slot: -// -// tx := cluster.TxPipeline() -// a := tx.Set("{user:123}:name", "Alice", 0) -// b := tx.Set("{user:123}:email", "alice@example.com", 0) -// if err := tx.Exec(ctx); err != nil { ... } -// -// [ClusterClient.Watch] validates slot affinity for the watched keys and -// delegates to the target node's [Client.Watch]: -// -// err := cluster.Watch(ctx, func(tx *redis.Tx) error { -// tx.Incr("{user:123}:visits") -// return tx.Exec(ctx) -// }, "{user:123}:visits") -// -// # Known limitations -// -// - TLS (rediss://) is not yet supported; the scheme is rejected in -// [NewClient] with a clear error. Deploy over VPC, loopback, or a -// sidecar TLS terminator. -// - Cluster ([ClusterClient]) and Sentinel ([SentinelClient]) are supported. -// MOVED/ASK redirects are handled transparently by ClusterClient; Sentinel -// auto-discovers the master and handles failovers via +switch-master. -// Cluster pipeline splits commands by slot and executes per-node -// sub-pipelines in parallel. SSUBSCRIBE (shard channels, Redis 7+) is -// not supported; use regular SUBSCRIBE which is cluster-wide. -// - No wrappers for RedisJSON, RediSearch, RedisGraph, or RedisTimeSeries — -// use [Client.Do] with the raw command strings. -// - Pub/Sub auto-reconnects but messages delivered while the conn is down -// are lost. Callers needing at-least-once should add server-side -// durability (Streams + consumer groups). -// - database/sql is not implemented — Redis is not a SQL database. -// - Read/write timeouts are advisory today; cancellation is via ctx. +// Key exported symbols and when to reach for them: +// +// - [Client] — typed commands for strings, hashes, lists, sets, sorted sets, +// keys, scripting (Eval, EvalSHA, ScriptLoad) and pub/sub. [Client.Do] +// (plus DoString/DoInt/DoBool/DoSlice) is the escape hatch for any command +// the typed surface omits. +// - [Client.Pipeline] returns a [Pipeline] that batches commands into one +// write; queued calls return typed handles (*StringCmd, *IntCmd, etc.) +// resolved after [Pipeline.Exec]. +// - [Client.TxPipeline] and [Client.Watch] provide MULTI/EXEC transactions +// and optimistic locking via [Tx]; aborts surface as [ErrTxAborted]. +// - [Client.Subscribe] / [Client.PSubscribe] return a [PubSub] whose +// [PubSub.Channel] delivers [Message]s, with automatic reconnect. +// - [Client.Scan] returns a [ScanIterator] for cursor-based key iteration. +// - [NewClusterClient] ([ClusterClient]) and [NewSentinelClient] +// ([SentinelClient]) cover Redis Cluster and Sentinel deployments. +// +// Typed accessors and pipeline/Do results are detached from the reader buffer, +// so returned values are safe to retain. Server error replies surface as +// [*RedisError] (with sentinels such as [ErrNil], [ErrWrongType], +// [ErrCrossSlot]); see [errors.Is]. TLS (rediss://) is not yet supported. +// +// # Documentation +// +// Full guides and examples: https://goceleris.dev/docs/data-stores package redis diff --git a/engine/doc.go b/engine/doc.go index df2b60ae..f1349c85 100644 --- a/engine/doc.go +++ b/engine/doc.go @@ -4,4 +4,8 @@ // This package is the dependency root for engine-related types. User code // typically interacts with engines through the top-level celeris package // rather than importing this package directly. +// +// # Documentation +// +// Full guides and examples: https://goceleris.dev/docs/engines package engine diff --git a/middleware/adapters/doc.go b/middleware/adapters/doc.go index 96664814..84909eb1 100644 --- a/middleware/adapters/doc.go +++ b/middleware/adapters/doc.go @@ -1,68 +1,18 @@ -// Package adapters bridges stdlib net/http middleware into celeris handler chains. +// Package adapters bridges stdlib net/http middleware and handlers into celeris handler chains. // // Use [WrapMiddleware] to adapt any func(http.Handler) http.Handler into a -// [celeris.HandlerFunc]. This enables reuse of existing stdlib middleware -// such as rs/cors, gorilla/csrf, and similar libraries. +// [celeris.HandlerFunc], enabling reuse of existing stdlib middleware such as +// rs/cors, gorilla/csrf, and similar libraries. // -// corsHandler := cors.Handler(cors.Options{AllowedOrigins: []string{"*"}}) -// server.Use(adapters.WrapMiddleware(corsHandler)) +// Use [ReverseProxy] to forward requests to a backend URL, with optional hooks +// via [WithTransport], [WithModifyRequest], [WithModifyResponse], and +// [WithErrorHandler]. // -// Warning: Do not use both adapters.WrapMiddleware with a stdlib CORS -// library (e.g., rs/cors) AND the native celeris/middleware/cors in the -// same chain. This produces duplicate Access-Control-* headers and -// conflicting preflight handling. Use one or the other. +// Note: do not combine [WrapMiddleware] with a stdlib CORS library and the +// native celeris/middleware/cors in the same chain — this produces duplicate +// Access-Control-* headers and conflicting preflight handling. // -// For converting celeris handlers to stdlib, use [celeris.ToHandler] directly. +// # Documentation // -// # How It Works -// -// WrapMiddleware creates a temporary http.ResponseWriter (responseCapture) -// and reconstructs an *http.Request from the celeris Context. The stdlib -// middleware runs against these. If the middleware calls the inner handler, -// control returns to the celeris chain via c.Next(). Any response headers -// the stdlib middleware set before calling next are copied to the celeris -// response. -// -// If the stdlib middleware short-circuits (does not call the inner handler), -// the captured status code, headers, and body are written through celeris. -// -// # Performance -// -// WrapMiddleware reconstructs an *http.Request per call, which costs -// 8-15 heap allocations depending on header count and body presence. -// For hot-path middleware (e.g., CORS on every request), prefer the native -// celeris/middleware/cors over adapters.WrapMiddleware(rs/cors) for -// zero-alloc performance. Use WrapMiddleware for middleware that runs -// infrequently or where the stdlib library has no native celeris equivalent. -// -// # Limitations -// -// The responseCapture used by WrapMiddleware does not implement -// http.Hijacker or http.Flusher. Stdlib middleware that requires -// WebSocket upgrade (via Hijack) or streaming flush (via Flush) -// will not work through WrapMiddleware. For these use cases, -// implement the middleware natively in celeris. -// -// # Reverse Proxy -// -// [ReverseProxy] creates a handler that forwards requests to a target URL -// using [net/http/httputil.ReverseProxy] under the hood: -// -// target, _ := url.Parse("http://backend:8080") -// server.Any("/api/*path", adapters.ReverseProxy(target)) -// -// Options: -// -// - [WithTransport]: set a custom [http.RoundTripper] -// - [WithModifyRequest]: mutate outbound requests (e.g. add headers) -// - [WithModifyResponse]: inspect or modify backend responses before forwarding -// - [WithErrorHandler]: custom error handling for proxy failures -// -// The proxy automatically sets X-Forwarded-For, X-Forwarded-Host, and -// X-Forwarded-Proto via [net/http/httputil.ProxyRequest.SetXForwarded]. -// Panics if target is nil. -// -// ReverseProxy delegates to [celeris.Adapt], which buffers the response. -// Streaming responses (SSE, WebSocket upgrade) are not supported through -// the proxy. +// Full guides and examples: https://goceleris.dev/docs/net-http-interop package adapters diff --git a/middleware/basicauth/doc.go b/middleware/basicauth/doc.go index 124bf78d..06f83c68 100644 --- a/middleware/basicauth/doc.go +++ b/middleware/basicauth/doc.go @@ -1,57 +1,35 @@ // Package basicauth provides HTTP Basic Authentication middleware for // celeris. // -// The middleware parses the Authorization header via the framework's -// [celeris.Context.BasicAuth] method, validates credentials via a -// user-supplied function, and stores the authenticated username in the -// context store under [UsernameKey]. Failed authentication returns 401 -// with a WWW-Authenticate header. -// -// One of [Config].Validator, [Config].ValidatorWithContext, [Config].Users, -// or [Config].HashedUsers is required; omitting all four panics at -// initialization. -// -// Simple usage with a Users map (auto-generates a constant-time validator): +// The middleware parses the Authorization header via [celeris.Context.BasicAuth], +// validates credentials via a user-supplied function, and stores the +// authenticated username in the context store under [UsernameKey]. Failed +// authentication returns 401 with WWW-Authenticate, Cache-Control, and Vary +// headers. +// +// Exactly one credential source is required; [New] panics otherwise: +// - [Config].Users — plaintext map, auto-generates a constant-time HMAC validator. +// - [Config].HashedUsers + [Config].HashedUsersFunc — opaque hash strings with +// a caller-supplied compare function (bcrypt, argon2id, scrypt, etc.). +// HashedUsersFunc is mandatory when HashedUsers is set. +// - [Config].Validator — arbitrary func(user, pass string) bool. +// - [Config].ValidatorWithContext — same, with request context access. +// +// Minimal usage with a Users map: // // server.Use(basicauth.New(basicauth.Config{ // Users: map[string]string{ // "admin": "secret", -// "user": "pass", -// }, -// })) -// -// Hashed passwords with SHA-256 (avoids storing plaintext): -// -// server.Use(basicauth.New(basicauth.Config{ -// HashedUsers: map[string]string{ -// "admin": basicauth.HashPassword("secret"), // }, // })) // -// WARNING: SHA-256 is a fast hash with no work factor. For high-security -// password storage, use bcrypt, scrypt, or Argon2 via a custom -// [Config].Validator or [Config].HashedUsersFunc. -// -// Plugging in bcrypt via HashedUsersFunc: -// -// server.Use(basicauth.New(basicauth.Config{ -// HashedUsers: map[string]string{ -// "admin": "$2a$10$...", // bcrypt hash -// }, -// HashedUsersFunc: func(hash, password string) bool { -// return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil -// }, -// })) -// -// # Retrieving the Username -// -// Use [UsernameFromContext] to retrieve the authenticated username from -// downstream handlers: +// Use [UsernameFromContext] to retrieve the authenticated username downstream. +// Set [Config].Skip or [Config].SkipPaths to bypass the middleware selectively. // -// name := basicauth.UsernameFromContext(c) +// Note: [HashPassword] (SHA-256) is deprecated and credential-grade only with a +// modern KDF. Use bcrypt or argon2 via [Config].HashedUsersFunc instead. // -// # Skipping +// # Documentation // -// Set [Config].Skip to bypass the middleware dynamically, or -// [Config].SkipPaths for exact-match path exclusions. +// Full guides and examples: https://goceleris.dev/docs/middleware-auth package basicauth diff --git a/middleware/bodylimit/doc.go b/middleware/bodylimit/doc.go index e198c8d5..b191be91 100644 --- a/middleware/bodylimit/doc.go +++ b/middleware/bodylimit/doc.go @@ -1,50 +1,27 @@ // Package bodylimit provides request body size limiting middleware for // celeris. // -// The middleware enforces a maximum request body size using a two-phase -// approach: first checking Content-Length (fast path), then verifying -// actual body bytes (catches lying or absent Content-Length). Requests -// exceeding the limit are rejected with 413 Request Entity Too Large. -// -// Basic usage with the default 4 MB limit: -// -// server.Use(bodylimit.New()) -// -// Human-readable size string: -// -// server.Use(bodylimit.New(bodylimit.Config{ -// Limit: "10MB", -// })) -// -// [Config].Limit accepts SI and IEC units: B, KB, MB, GB, TB, PB, EB, -// KiB, MiB, GiB, TiB, PiB, EiB. Fractional values are supported. -// When set, Limit takes precedence over MaxBytes. -// -// LAYER OF DEFENSE — read carefully: -// -// The DoS-grade cap lives at the engine read layer: -// [celeris.Config.MaxRequestBodySize] (default 100 MB). Bodies larger -// than that are rejected by the engine BEFORE any buffering happens, so -// the framework never holds them in memory. Set -// [celeris.Config.MaxRequestBodySize] to your real maximum at server -// construction time. -// -// This middleware runs AFTER the engine has already buffered the body. -// Use it for per-route caps that are smaller than the server-wide -// MaxRequestBodySize (e.g. a /comments endpoint that should accept at -// most 64 KB while /uploads accepts up to MaxRequestBodySize). It is -// NOT a substitute for setting MaxRequestBodySize itself; a malicious -// client can still send up to MaxRequestBodySize before this middleware -// rejects them. -// -// Enable [Config].ContentLengthRequired to reject requests without -// Content-Length (411 Length Required), forcing clients to declare the -// body size up-front. -// -// Bodyless methods (GET, HEAD, DELETE, OPTIONS, TRACE, CONNECT) are -// auto-skipped. Use [Config].Skip or [Config].SkipPaths for additional -// exclusions. Set [Config].ErrorHandler to customize error responses. -// -// [ErrBodyTooLarge] and [ErrLengthRequired] are exported sentinel errors -// usable with errors.Is for upstream error handling. +// [New] returns a [celeris.HandlerFunc] that enforces a maximum request body +// size using a two-phase check: Content-Length header (fast path) then actual +// body bytes (catches lying or absent Content-Length). Requests exceeding the +// limit are rejected with 413 Request Entity Too Large. +// +// Configure via [Config]: set [Config].Limit to a human-readable size string +// (e.g. "10MB", "1.5GiB"; SI and IEC units accepted; takes precedence over +// [Config].MaxBytes), or set [Config].MaxBytes directly (default 4 MiB). +// Enable [Config].ContentLengthRequired to reject requests that omit +// Content-Length with 411 Length Required. Bodyless methods (GET, HEAD, +// DELETE, OPTIONS, TRACE, CONNECT) are auto-skipped; use [Config].Skip or +// [Config].SkipPaths for additional exclusions. Customize error responses with +// [Config].ErrorHandler. Sentinel errors [ErrBodyTooLarge] and +// [ErrLengthRequired] are usable with errors.Is. +// +// Note: this middleware runs after the engine has buffered the body. It adds +// an application-level per-route cap below the server-wide engine limit +// (celeris.Config.MaxRequestBodySize, default 100 MiB); it is not a +// substitute for setting that limit. +// +// # Documentation +// +// Full guides and examples: https://goceleris.dev/docs/middleware-traffic package bodylimit diff --git a/middleware/cache/doc.go b/middleware/cache/doc.go index 7a29eda7..cf4a09fb 100644 --- a/middleware/cache/doc.go +++ b/middleware/cache/doc.go @@ -36,4 +36,8 @@ // - [Invalidate] removes a single computed key // - [InvalidatePrefix] removes every key with the given prefix // (requires store.PrefixDeleter) +// +// # Documentation +// +// Full guides and examples: https://goceleris.dev/docs/middleware-content package cache diff --git a/middleware/circuitbreaker/doc.go b/middleware/circuitbreaker/doc.go index 504f0a6a..29de236e 100644 --- a/middleware/circuitbreaker/doc.go +++ b/middleware/circuitbreaker/doc.go @@ -1,111 +1,26 @@ // Package circuitbreaker provides circuit breaker middleware for celeris. // -// The circuit breaker monitors downstream error rates using a sliding window -// and automatically stops sending requests when failure thresholds are exceeded, -// giving the failing service time to recover. +// The breaker monitors downstream error rates using a sliding window and +// automatically stops sending requests when failure thresholds are exceeded, +// giving the failing service time to recover. It operates as a three-state +// machine: Closed (normal), Open (rejecting all requests with 503), and +// HalfOpen (allowing limited probe requests to test recovery). // -// # Three-State Machine +// Use [New] to create middleware with default settings (50% threshold, +// minimum 10 requests, 10 s window, 30 s cooldown). Pass a [Config] to +// tune [Config.Threshold], [Config.MinRequests], [Config.WindowSize], +// [Config.CooldownPeriod], [Config.HalfOpenMax], [Config.IsError], and +// [Config.OnStateChange]. // -// The breaker operates in three states: +// Use [NewWithBreaker] to obtain a [*Breaker] reference for programmatic +// state inspection ([Breaker.State]), sliding-window counter export +// ([Breaker.Counts]), and forced reset ([Breaker.Reset]) from health-check +// or admin handlers. // -// - Closed: All requests pass through. The sliding window tracks successes -// and failures. When the failure ratio meets or exceeds [Config].Threshold -// (and at least [Config].MinRequests have been observed), the breaker -// trips to Open. +// [ErrServiceUnavailable] is the sentinel error (503) returned when the +// breaker is open; use errors.Is to match it in upstream middleware. // -// - Open: All requests are immediately rejected with 503 Service Unavailable. -// A Retry-After header is set with the remaining cooldown seconds. After -// [Config].CooldownPeriod elapses, the breaker transitions to HalfOpen. +// # Documentation // -// - HalfOpen: Up to [Config].HalfOpenMax probe requests are allowed through. -// If a probe succeeds, the breaker returns to Closed. If a probe fails, -// the breaker returns to Open. Excess requests beyond HalfOpenMax are -// rejected with 503. -// -// # Sliding Window Algorithm -// -// The observation window is divided into 10 time buckets spanning -// [Config].WindowSize. Each bucket tracks successes and failures using atomic -// counters. Expired buckets (older than WindowSize) are excluded from the -// error rate calculation. This provides a smooth, time-decaying view of -// the failure rate without requiring a global lock on the hot path. -// -// # Response Classification -// -// By default, responses with status >= 500 are counted as failures. Use -// [Config].IsError to customize classification (e.g., treat 429 as a failure, -// or ignore certain 5xx codes). -// -// # Retry-After Header -// -// When the breaker is open, the middleware sets a Retry-After header with -// the number of seconds until the cooldown period expires. Clients that -// respect this header avoid unnecessary retries during the cooldown. -// -// # Programmatic Access -// -// Use [NewWithBreaker] to obtain a reference to the underlying [Breaker] -// struct. This allows programmatic state inspection ([Breaker.State]), -// window counter export ([Breaker.Counts]) for dashboards and Prometheus, -// and forced reset ([Breaker.Reset]) for health checks, admin endpoints, -// or integration tests. -// -// # Middleware Ordering -// -// Place the circuit breaker after rate limiting and before timeout middleware: -// -// server.Use(ratelimit.New()) -// server.Use(circuitbreaker.New()) -// server.Use(timeout.New()) -// -// This ensures rate-limited requests are rejected before reaching the breaker, -// and timed-out requests are properly classified by the breaker. -// -// # In-Flight Requests During Transition -// -// Requests that are already executing when the breaker transitions to Open -// continue to completion — only NEW requests are rejected. This is by design: -// interrupting in-flight requests could cause data corruption or incomplete -// operations. -// -// # Per-Endpoint Breakers -// -// To use separate breakers for different services or route groups, create -// multiple instances and apply them to the appropriate groups: -// -// payments := s.Group("/api/payments") -// payments.Use(circuitbreaker.New(circuitbreaker.Config{Threshold: 0.3})) -// -// # Thread Safety -// -// The breaker is safe for concurrent use. State reads and counter updates -// use atomic operations (lock-free hot path). State transitions acquire a -// mutex with double-check locking to prevent duplicate transitions. -// -// Basic usage with defaults (50% threshold, minimum 10 requests, 10s window): -// -// server.Use(circuitbreaker.New()) -// -// Custom threshold and cooldown: -// -// server.Use(circuitbreaker.New(circuitbreaker.Config{ -// Threshold: 0.3, -// MinRequests: 20, -// CooldownPeriod: time.Minute, -// })) -// -// Programmatic access for health checks: -// -// mw, breaker := circuitbreaker.NewWithBreaker() -// server.Use(mw) -// server.GET("/health", func(c *celeris.Context) error { -// if breaker.State() == circuitbreaker.Open { -// return c.JSON(503, map[string]string{"circuit": "open"}) -// } -// return c.JSON(200, map[string]string{"circuit": "closed"}) -// }) -// -// [ErrServiceUnavailable] is the exported sentinel error (503) returned when -// the breaker is open, usable with errors.Is for error handling in upstream -// middleware. +// Full guides and examples: https://goceleris.dev/docs/middleware-traffic package circuitbreaker diff --git a/middleware/compress/doc.go b/middleware/compress/doc.go index 6a8551c5..2bac8b85 100644 --- a/middleware/compress/doc.go +++ b/middleware/compress/doc.go @@ -1,169 +1,34 @@ // Package compress provides transparent response compression middleware // for celeris. // -// Supported encodings are zstd, brotli, gzip, and deflate, negotiated via -// the Accept-Encoding request header. The server-side priority order is -// configurable; the default prefers zstd > brotli > gzip. Deflate is -// supported but opt-in only (not in the default Encodings list) because -// it is a legacy encoding superseded by gzip. +// It negotiates an encoding from the Accept-Encoding request header and +// compresses 2xx responses on the fly. Supported encodings are zstd, brotli +// ("br"), gzip, and deflate; the default server-side priority is +// zstd > br > gzip. Deflate is supported but opt-in (add "deflate" to +// [Config].Encodings) because it is superseded by gzip. // -// Basic usage with defaults (zstd > br > gzip, MinLength 256): +// [New] returns the middleware; call it with no arguments for defaults +// (MinLength 256, the default encoding list and excluded content types) or +// pass a [Config] to tune behavior: // // server.Use(compress.New()) // -// Gzip-only with fastest compression: +// Per-encoding levels are set via [Config].GzipLevel, [Config].BrotliLevel, +// [Config].ZstdLevel, and [Config].DeflateLevel, each accepting a [Level] +// (LevelDefault, LevelFastest, LevelBest, LevelNone) or an integer in the +// encoding's valid range. Use [Config].Skip, [Config].SkipPaths, +// [Config].MinLength, and [Config].ExcludedContentTypes to control what gets +// compressed. // -// server.Use(compress.New(compress.Config{ -// Encodings: []string{"gzip"}, -// GzipLevel: compress.LevelFastest, -// })) +// The middleware buffers the response body before compressing, so it does not +// apply to handlers that use [celeris.Context.StreamWriter]. For streaming +// endpoints, wrap a StreamWriter with [NewCompressedStream] (gzip or brotli +// only), or bypass compression with [Config].Skip. // -// Per-encoding compression levels are configurable via [Config].GzipLevel, -// [Config].BrotliLevel, and [Config].ZstdLevel. Each accepts [Level] -// constants (LevelDefault, LevelFastest, LevelBest, LevelNone) or an -// integer within the encoding's valid range. +// This package is a separate Go module (middleware/compress/go.mod); import +// it as "github.com/goceleris/celeris/middleware/compress". // -// Exclude specific content types and paths: +// # Documentation // -// server.Use(compress.New(compress.Config{ -// ExcludedContentTypes: []string{"image/", "video/", "audio/", "application/octet-stream"}, -// SkipPaths: []string{"/health", "/metrics"}, -// })) -// -// # Method Filtering -// -// HEAD and OPTIONS requests bypass compression entirely (only Vary is added). -// This avoids unnecessary buffering for requests that do not produce -// response bodies. -// -// # Content-Negotiation -// -// The middleware calls [celeris.Context.AcceptsEncodings] with the -// configured encoding list. If the client does not accept any supported -// encoding, the response passes through uncompressed. -// -// # Status Code Range -// -// Only 2xx responses are compressed. Error responses (4xx, 5xx), redirects -// (3xx), and informational responses (1xx) pass through uncompressed. -// -// # Deflate (opt-in) -// -// The "deflate" encoding (raw DEFLATE, RFC 1951) is supported but not -// included in the default Encodings list. Add it explicitly: -// -// server.Use(compress.New(compress.Config{ -// Encodings: []string{"zstd", "br", "gzip", "deflate"}, -// })) -// -// Deflate is a legacy encoding. Prefer gzip, brotli, or zstd for new -// deployments. -// -// # Streaming Compression -// -// For endpoints that produce large or streaming responses, use -// [NewCompressedStream] to wrap a [celeris.StreamWriter] with on-the-fly -// gzip or brotli compression. This avoids buffering the entire response -// body but does not support the expansion guard or MinLength threshold. -// -// # Pool Strategy -// -// Writer pools eliminate per-request allocations on the hot path: -// -// - gzip: [sync.Pool] of *gzip.Writer — Get, Reset, Write, Close, Put. -// - brotli: [sync.Pool] of *brotli.Writer — same pattern. -// - deflate: [sync.Pool] of *flate.Writer — same pattern. -// - zstd: single thread-safe [zstd.Encoder] — EncodeAll, no pool needed. -// - buffers: [sync.Pool] of *bytes.Buffer for gzip/brotli/deflate output. -// -// # StreamWriter Incompatibility -// -// This middleware uses [celeris.Context.BufferResponse] which is incompatible -// with [celeris.Context.StreamWriter]. If the downstream handler uses -// StreamWriter, BufferResponse returns nil and the streamed response -// bypasses this middleware entirely. Use [Config].Skip to explicitly -// exclude streaming endpoints. -// -// # Response Buffering (No Streaming Compression) -// -// This middleware buffers the entire response body in memory before -// deciding whether and how to compress. This design enables the expansion -// guard (flush original if compressed >= original) and correct -// Content-Length headers, but means the full uncompressed body must fit -// in memory. -// -// For endpoints that produce large responses (file downloads, CSV exports, -// streaming JSON), use [Config].Skip or [Config].SkipPaths to bypass -// compression entirely. Consider using the framework's StreamWriter for -// such endpoints instead. -// -// compress.New(compress.Config{ -// SkipPaths: []string{"/api/export", "/files"}, -// Skip: func(c *celeris.Context) bool { -// return strings.HasPrefix(c.Path(), "/download/") -// }, -// }) -// -// Competing frameworks (Echo, Fiber) offer streaming compression by -// wrapping the response writer, but this prevents expansion detection -// and accurate Content-Length. -// -// # MinLength Threshold -// -// Responses smaller than [Config].MinLength bytes (default 256) are not -// compressed. Set MinLength to 0 to compress all non-empty responses. -// This avoids wasting CPU on responses too small to benefit from compression. -// -// # Excluded Content Types -// -// Content types matching [Config].ExcludedContentTypes prefixes are -// skipped. The default excludes "image/", "video/", and "audio/" since -// these are typically already compressed. -// -// # Vary Header -// -// The middleware appends "Accept-Encoding" to the Vary header using -// [celeris.Context.AddHeader] so that existing Vary values (e.g., Origin -// from CORS) are preserved. The Vary header is added on all non-skipped -// responses, including those that pass through uncompressed (below -// MinLength, excluded content type, etc.), so that caches correctly -// distinguish responses that vary by encoding. -// -// # Compression Expansion Guard -// -// If the compressed output is equal to or larger than the original body, -// the original is sent uncompressed. This prevents pathological expansion -// on already-compressed or incompressible data. -// -// # Ordering with ETag -// -// Compress runs OUTSIDE etag middleware. The recommended chain is: -// -// server.Use(compress.New()) // outermost -// server.Use(etag.New()) // inner — ETag computed on uncompressed body -// -// # BREACH Attack Warning -// -// Compressing HTTPS responses that reflect user input (search queries, -// form values, URL parameters) alongside secrets (CSRF tokens, session -// IDs) can leak those secrets via the BREACH attack. Mitigation options: -// - Exclude sensitive endpoints with [Config].SkipPaths or [Config].Skip -// - Randomize padding on pages that mix user input and secrets -// - Separate secret-bearing responses from user-controlled content -// -// See http://breachattack.com for details. This applies to any HTTP -// compression layer, not just this middleware. -// -// # Response Size Measurement -// -// When metrics or OTel middleware runs before compress (the recommended -// ordering), response size metrics reflect uncompressed application-level -// sizes. See middleware/metrics and middleware/otel documentation. -// -// # Separate Sub-Module -// -// This package is a separate Go module (middleware/compress/go.mod) with -// its own dependency set. Import it as: -// -// import "github.com/goceleris/celeris/middleware/compress" +// Full guides and examples: https://goceleris.dev/docs/middleware-content package compress diff --git a/middleware/cors/doc.go b/middleware/cors/doc.go index d122406b..6df31468 100644 --- a/middleware/cors/doc.go +++ b/middleware/cors/doc.go @@ -1,87 +1,25 @@ // Package cors provides Cross-Origin Resource Sharing (CORS) middleware -// for celeris. +// for Celeris. // -// The middleware handles preflight OPTIONS requests (detected via -// OPTIONS method + Access-Control-Request-Method header) and sets the -// appropriate Access-Control-* response headers. Header values are -// pre-joined at initialization for zero-alloc responses on the hot path. +// Call [New] with an optional [Config] to create a middleware handler. +// With no arguments, all origins are allowed (AllowOrigins: ["*"]). +// Pass a Config to restrict origins, enable credentials, set MaxAge, and more. // -// Allow all origins (default): +// Key Config fields: +// - AllowOrigins: static origin list; supports subdomain wildcards ("https://*.example.com"). +// - AllowOriginsFunc: custom callback to validate an origin string. +// - AllowOriginRequestFunc: like AllowOriginsFunc but receives the full request context. +// - AllowCredentials: set true to emit Access-Control-Allow-Credentials. +// - MirrorRequestHeaders: reflect Access-Control-Request-Headers back on preflight. +// - AllowPrivateNetwork: emit Access-Control-Allow-Private-Network on preflight. +// - Skip / SkipPaths: bypass CORS processing for selected requests or paths. // -// server.Use(cors.New()) +// AllowOriginsFunc and AllowOriginRequestFunc cannot be combined with a wildcard +// ("*") AllowOrigins entry. Using AllowCredentials with "*" panics at New. +// Subdomain wildcards with AllowCredentials also panic unless +// UnsafeAllowCredentialsWithWildcard is set. // -// Restrict to specific origins with credentials: +// # Documentation // -// server.Use(cors.New(cors.Config{ -// AllowOrigins: []string{"https://example.com"}, -// AllowCredentials: true, -// MaxAge: 3600, -// })) -// -// # Dynamic Origin Validation -// -// [Config].AllowOriginsFunc validates origins with a custom function. -// [Config].AllowOriginRequestFunc provides the full request context for -// tenant-based or header-dependent origin checks. Neither may be combined -// with a wildcard ("*") AllowOrigins entry. -// -// server.Use(cors.New(cors.Config{ -// AllowOriginsFunc: func(origin string) bool { -// return strings.HasSuffix(origin, ".example.com") -// }, -// })) -// -// # Subdomain Wildcards -// -// Origins may contain a single wildcard for subdomain matching -// (e.g., "https://*.example.com"). Patterns with more than one wildcard -// panic at initialization. By default, the wildcard matches a single -// subdomain level only: "https://*.example.com" matches -// "https://api.example.com" but NOT "https://a.b.example.com". This -// prevents unintended deep-subdomain matching that could weaken origin -// restrictions. The depth limit is enforced by counting dots in the -// wildcard portion (max 0 additional dots for depth 1). -// -// # Cache Poisoning Prevention -// -// When specific origins (not wildcard "*") are configured, the middleware -// adds a Vary: Origin header even on non-CORS requests (those without an -// Origin header). This prevents intermediate caches from serving a -// response generated for one origin to a different origin. -// -// # Header Mirroring -// -// When [Config].MirrorRequestHeaders is true, the middleware mirrors the -// value of the Access-Control-Request-Headers header from the preflight -// request back in Access-Control-Allow-Headers. Each header name is -// validated against the RFC 7230 token charset, names longer than 128 -// bytes are silently dropped, and at most 20 header names are accepted. -// When MirrorRequestHeaders is false (the default), the AllowHeaders -// list is used (defaults to Origin, Content-Type, Accept, Authorization). -// -// # Private Network Access -// -// Set [Config].AllowPrivateNetwork to true to enable the Private Network -// Access spec (Access-Control-Allow-Private-Network header on preflight). -// -// # Unsafe Credentials with Wildcard Subdomains -// -// Using AllowCredentials with a subdomain wildcard origin -// (e.g., "https://*.example.com") panics by default because the browser -// receives the echoed origin (not "*") with credentials, widening the -// credential scope to every matching subdomain. Set -// [Config].UnsafeAllowCredentialsWithWildcard to true to suppress the -// panic if you fully understand the security implications. -// -// # Null Origin Warning -// -// The literal "null" origin is sent by sandboxed iframes, data: URLs, -// file:// pages, and redirected requests. Because all of these share the -// same "null" string, allowing it effectively grants access to any -// sandboxed context. Treat "null" as an opaque origin and avoid adding -// it to AllowOrigins unless you fully understand the implications. -// AllowOriginsFunc is a safer alternative for case-by-case decisions. -// -// Using AllowCredentials with a wildcard origin ("*") panics at -// initialization, as this combination is forbidden by the CORS spec. +// Full guides and examples: https://goceleris.dev/docs/middleware-security package cors diff --git a/middleware/csrf/doc.go b/middleware/csrf/doc.go index ffcbaaea..06edd305 100644 --- a/middleware/csrf/doc.go +++ b/middleware/csrf/doc.go @@ -2,87 +2,31 @@ // for celeris using the double-submit cookie pattern, with optional // server-side token storage for enhanced security. // -// On safe HTTP methods (GET, HEAD, OPTIONS, TRACE by default) the -// middleware generates or reuses a CSRF token, sets it as a cookie, -// and stores it in the request context. On unsafe methods (POST, PUT, -// DELETE, PATCH) it performs defense-in-depth checks (Sec-Fetch-Site, -// Origin, Referer) then compares the cookie token against the request -// token using constant-time comparison. -// -// Default usage (token in X-CSRF-Token header): -// -// server.Use(csrf.New()) -// -// Custom token lookup from a form field: -// -// server.Use(csrf.New(csrf.Config{ -// TokenLookup: "form:_csrf", -// })) -// -// # Retrieving the Token -// -// Use [TokenFromContext] to read the token in downstream handlers: -// -// token := csrf.TokenFromContext(c) -// -// # Server-Side Token Storage -// -// For enhanced security, configure [Config].Storage to validate tokens -// against a server-side store (signed double-submit): -// -// store := csrf.NewMemoryStorage() -// server.Use(csrf.New(csrf.Config{ -// Storage: store, -// Expiration: 2 * time.Hour, -// })) -// -// # Session Cookies -// -// Set [Config].CookieMaxAge to 0 for a browser-session-scoped cookie: -// -// server.Use(csrf.New(csrf.Config{ -// CookieMaxAge: 0, -// })) -// -// # Trusted Origins -// -// [Config].TrustedOrigins allows cross-origin requests from specific -// origins. Wildcard subdomain patterns are supported: -// -// server.Use(csrf.New(csrf.Config{ -// TrustedOrigins: []string{ -// "https://app.example.com", -// "https://*.example.com", -// }, -// })) -// -// # Sentinel Errors -// -// The package exports sentinel errors ([ErrForbidden], [ErrMissingToken], -// [ErrTokenNotFound], [ErrOriginMismatch], [ErrRefererMissing], -// [ErrRefererMismatch], [ErrSecFetchSite]) for use with errors.Is. -// -// # Security -// -// CookieSecure defaults to false for development convenience. Production -// deployments MUST set CookieSecure: true to prevent cookie transmission -// over unencrypted connections. -// -// CookieHTTPOnly is always enforced as true regardless of the user-supplied -// Config value. This prevents client-side JavaScript from reading the CSRF -// cookie, which is a defense-in-depth measure against XSS token theft. -// -// # Method Override Interaction -// -// When using the methodoverride middleware (registered via Server.Pre()), -// the HTTP method is rewritten before CSRF validation. A POST request with -// X-HTTP-Method-Override: PUT becomes a PUT by the time CSRF runs. -// -// IMPORTANT: Do not add PUT, DELETE, or PATCH to SafeMethods if you use -// method override. Doing so would allow form submissions to bypass CSRF -// token validation by tunneling through POST → PUT/DELETE/PATCH. -// -// The default SafeMethods (GET, HEAD, OPTIONS, TRACE) are safe because -// methodoverride only overrides POST requests, and the typical override -// targets (PUT, DELETE, PATCH) are not in the CSRF safe set. +// On safe methods (GET, HEAD, OPTIONS, TRACE) the middleware generates or +// reuses a CSRF token, sets it as a cookie, and stores it in the request +// context. On unsafe methods it performs defense-in-depth checks +// (Sec-Fetch-Site, Origin, Referer) then compares the cookie token against +// the submitted token using constant-time comparison. +// +// Key exported symbols: +// - [New] — constructs the middleware; accepts an optional [Config]. +// - [Config] — controls token lookup (header/form/query), cookie attributes, +// trusted origins, server-side [Config.Storage], and [Config.SingleUseToken]. +// - [TokenFromContext] — retrieves the current CSRF token from a handler. +// - [HandlerFromContext] — returns the [Handler] for the active middleware +// instance; use [Handler.DeleteToken] on logout. +// - [DeleteToken] — package-level convenience wrapper for the above. +// - Sentinel errors ([ErrForbidden], [ErrMissingToken], [ErrTokenNotFound], +// [ErrOriginMismatch], [ErrRefererMissing], [ErrRefererMismatch], +// [ErrSecFetchSite]) for use with errors.Is. +// +// Production note: [Config.CookieSecure] defaults to false for development +// convenience; set it to true in production. [Config.CookieHTTPOnly] is +// always enforced as true regardless of the supplied value. When using the +// methodoverride middleware, do not add PUT, DELETE, or PATCH to +// [Config.SafeMethods] — doing so would allow bypass via POST tunneling. +// +// # Documentation +// +// Full guides and examples: https://goceleris.dev/docs/middleware-security package csrf diff --git a/middleware/debug/doc.go b/middleware/debug/doc.go index 0f5b1602..9922ede0 100644 --- a/middleware/debug/doc.go +++ b/middleware/debug/doc.go @@ -35,4 +35,8 @@ // // Server and Collector are optional; when nil, /routes returns [] and // /metrics returns 501. +// +// # Documentation +// +// Full guides and examples: https://goceleris.dev/docs/observability package debug diff --git a/middleware/doc.go b/middleware/doc.go index 6fec739e..88114c16 100644 --- a/middleware/doc.go +++ b/middleware/doc.go @@ -1,147 +1,24 @@ -// Package middleware provides production-ready middleware for celeris. +// Package middleware is the umbrella for celeris's production-ready middleware catalog. // -// # Pre-Routing Middleware (Server.Pre) +// It declares no exported symbols of its own. Each piece of middleware lives in +// its own subpackage and exposes a New constructor returning a +// celeris.HandlerFunc (for example cors.New, jwt.New, ratelimit.New, +// compress.New). Install them at one of two points: // -// These run before the router matches the request and can mutate the -// request method, path, scheme, host, or client IP: +// - Server.Use installs route middleware that runs after the router matches a +// request (logging, recovery, auth, CORS, rate limiting, compression, ...). +// - Server.Pre installs pre-routing middleware that runs before matching and +// may mutate the request method, path, scheme, host, or client IP +// (proxy, redirect, rewrite, methodoverride). Pre-routing middleware that +// writes a response MUST return without calling c.Next(). // -// proxy — extract real client IP/scheme/host from trusted proxy headers -// redirect — HTTPS, www, trailing-slash URL normalization -// rewrite — regex-based URL rewriting (pattern → replacement) -// methodoverride — override POST method via _method form field or header +// Ordering matters: each layer should see the context the layers before it +// established. See the documentation hub below for the recommended install +// order, per-middleware configuration, and cross-cutting conventions (auth +// stacking, the Vary header contract, and how the observe/metrics/otel +// measurement systems relate). // -// Order matters: install proxy first (sets real client IP/scheme for all -// downstream middleware), then redirect (uses scheme from proxy), then -// rewrite (modifies path after redirect normalizes the URL), then -// methodoverride (after path is finalized). +// # Documentation // -// Short-circuit contract: writing a response in pre-routing middleware does -// NOT auto-abort the chain. Custom pre-routing middleware that responds -// (e.g. a redirect or 4xx error) MUST return WITHOUT calling c.Next() — the -// shipped redirect middleware does this. If a pre-middleware writes a body -// AND calls c.Next(), the router will run and may write a second response. -// -// # Recommended Middleware Ordering (Server.Use) -// -// Install middleware in this order so each layer sees the right context: -// -// healthcheck — health probes respond early; place first to skip downstream middleware -// (install with Server.Use, NEVER with Server.Pre — pre-routing -// rewrite rules could otherwise retarget the probe paths) -// requestid — assign request ID first; all downstream logs include it -// metrics/otel — Prometheus / OpenTelemetry: can also go after logger -// logger — log every request with the ID from requestid -// recovery — catch panics from everything below; logger records the 500 -// secure — set OWASP security headers before any response can escape -// cors — handle preflight; must run before auth rejects OPTIONS -// bodylimit — reject oversized bodies before parsing begins -// ratelimit — shed load before expensive auth/business logic -// circuitbreaker — trip open on error rate spike; after ratelimit, before timeout -// [auth] — jwt / keyauth / basicauth (see Auth Stacking below) -// csrf — validate CSRF token after authentication is established -// session — load session (may depend on authenticated user) -// debug — intercepts by path prefix (e.g. /debug/); can go anywhere -// pprof — Go profiling endpoints (loopback-only by default) -// swagger — OpenAPI spec + UI (CDN-loaded Swagger UI or Scalar) -// static — static file server with directory browse (after security/auth if protecting) -// adapters — (utility) stdlib ↔ celeris middleware conversion; not a chain member -// timeout — bound handler execution; innermost wrapper before the route -// singleflight — collapse identical in-flight requests; after timeout -// compress — response compression; wraps etag (computes on uncompressed body) -// etag — conditional responses (304 Not Modified); innermost transform -// [handler] — route handler -// -// Example production stack: -// -// // Pre-routing -// s.Pre(proxy.New(proxy.Config{TrustedProxies: []string{"10.0.0.0/8"}})) -// s.Pre(redirect.HTTPSRedirect()) -// s.Pre(rewrite.New(rewrite.Config{Rules: []rewrite.Rule{{Pattern: `^/old/(.*)$`, Replacement: "/new/$1"}}})) -// s.Pre(methodoverride.New()) -// -// // Route middleware (install per the ordering list above; entries -// // commented out below are optional but, when used, must keep their -// // position in the chain). -// s.Use(healthcheck.New()) -// s.Use(requestid.New()) -// // s.Use(metrics.New(...)) // optional: Prometheus -// // s.Use(otel.New(...)) // optional: OpenTelemetry -// // s.Use(logger.New()) // optional: structured request logs -// s.Use(recovery.New()) -// s.Use(secure.New()) -// s.Use(cors.New()) -// // s.Use(bodylimit.New(...)) // optional: cap request body -// // s.Use(ratelimit.New(...)) // optional: shed load -// s.Use(circuitbreaker.New()) -// // s.Use(jwt.New(...)) // optional: auth (see Auth Stacking) -// // s.Use(csrf.New()) // optional: after auth -// // s.Use(session.New(...)) // optional: after auth -// s.Use(timeout.New(timeout.Config{Timeout: 30 * time.Second})) -// s.Use(singleflight.New()) -// s.Use(compress.New()) -// s.Use(etag.New()) -// -// # Measurement System Roles -// -// Three complementary systems serve different operational needs: -// -// Core Collector (github.com/goceleris/celeris/observe) — Built into the -// framework. Tracks total requests, error counts, and latency percentiles -// including unmatched routes and panic recoveries. Zero external dependencies. -// Use for lightweight internal diagnostics and health checks. -// -// Prometheus middleware (github.com/goceleris/celeris/middleware/metrics) — Production -// metrics pipeline. Emits per-path, per-method, per-status histograms and -// counters to Prometheus. Integrates with Grafana dashboards and alerting. -// Use for production monitoring and SLO tracking. -// -// OpenTelemetry middleware (github.com/goceleris/celeris/middleware/otel) — Distributed -// tracing and metrics. Creates spans per request with W3C trace context -// propagation. Exports to any OTLP-compatible backend (Jaeger, Tempo, etc.). -// Use for cross-service request correlation and latency breakdown. -// -// Counter overlap: each system records the same request independently — -// observe.Collector.TotalRequests, prometheus celeris_requests_total, and -// otel http.server.request.duration count are NOT shared. Enabling all -// three gives three independent views of the same traffic; do NOT add -// numbers across systems. Pick one as the source of truth for a given -// chart, alert, or SLO. -// -// # Auth Stacking Pattern -// -// JWT and keyauth both support ContinueOnIgnoredError, which calls c.Next() -// when ErrorHandler returns nil (i.e., the error was intentionally ignored). -// Chain them for JWT-preferred authentication with API key fallback: -// -// jwtAuth := jwt.New(jwt.Config{ -// SigningKey: hmacSecret, -// ContinueOnIgnoredError: true, -// ErrorHandler: func(c *celeris.Context, err error) error { -// return nil // ignore JWT failure, let keyauth try -// }, -// }) -// keyAuth := keyauth.New(keyauth.Config{ -// Validator: func(c *celeris.Context, key string) (bool, error) { -// return key == apiKey, nil -// }, -// }) -// api := s.Group("/api", jwtAuth, keyAuth) -// -// Requests with a valid JWT proceed after jwtAuth. Requests without a JWT -// (or with an invalid one) fall through to keyAuth. If neither succeeds, -// keyAuth returns 401. -// -// # Vary Header Convention -// -// Several middleware set the Vary response header: -// -// - cors: Vary: Origin -// - compress: Vary: Accept-Encoding -// -// All middleware use AddHeader (not SetHeader) for Vary to preserve values -// set by other middleware. Handlers that need to set Vary MUST also use -// AddHeader to avoid clobbering middleware-set values: -// -// c.AddHeader("vary", "Accept-Language") // correct -// c.SetHeader("vary", "Accept-Language") // WRONG: clobbers cors/compress Vary +// Full guides and examples: https://goceleris.dev/docs/middleware package middleware diff --git a/middleware/etag/doc.go b/middleware/etag/doc.go index fea1d4ab..623ca743 100644 --- a/middleware/etag/doc.go +++ b/middleware/etag/doc.go @@ -1,136 +1,26 @@ -// Package etag provides automatic ETag generation and conditional response -// middleware for celeris. -// -// The middleware computes a CRC-32 checksum of the response body and sets -// an ETag header. On subsequent requests with a matching If-None-Match -// header, it returns 304 Not Modified with no body, saving bandwidth. -// -// Basic usage: -// -// server.Use(etag.New()) -// -// Strong ETags (byte-for-byte identical guarantee): -// -// server.Use(etag.New(etag.Config{Strong: true})) -// -// # Algorithm -// -// The ETag value is computed using CRC-32 (IEEE polynomial) of the full -// response body. By default, weak ETags are generated in the format -// W/"xxxxxxxx" where xxxxxxxx is the hex-encoded checksum. Strong ETags -// omit the W/ prefix: "xxxxxxxx". -// -// CRC-32 is chosen for speed (single pass, no allocations beyond the -// fixed-size stack buffer). It is not cryptographically secure, but -// ETag is a cache validation mechanism, not a security primitive. -// -// # Custom Hash Function -// -// Use [Config].HashFunc to supply a custom hash. The function receives the -// full response body and returns the opaque-tag string (without quotes or -// W/ prefix -- those are added automatically based on the Strong setting): -// -// server.Use(etag.New(etag.Config{ -// HashFunc: func(body []byte) string { -// h := sha256.Sum256(body) -// return hex.EncodeToString(h[:16]) -// }, -// })) -// -// # Handler-Set ETags -// -// If the downstream handler already sets an ETag header, the middleware -// respects it and does not recompute. The existing ETag is still checked -// against If-None-Match for conditional 304 responses. -// -// # Weak Comparison (RFC 7232 Section 2.3.2) -// -// If-None-Match uses weak comparison: the W/ prefix is stripped before -// comparing opaque-tags. This means W/"abc" matches both W/"abc" and -// "abc", per the RFC. -// -// # If-None-Match: * -// -// The wildcard value "*" matches any ETag, unconditionally returning 304. -// -// # If-Match (Not Supported) -// -// This middleware does not handle If-Match (RFC 7232 Section 3.1). If-Match -// is used for conditional writes (PUT/DELETE) and should be validated at -// the application layer. The middleware only processes GET/HEAD requests. -// -// # If-None-Match Lenient Parsing -// -// The If-None-Match parser accepts unquoted tokens in addition to properly -// quoted ETag values. While RFC 7232 specifies that entity-tags must be -// quoted strings, real-world clients and proxies occasionally emit bare -// tokens. The parser handles these gracefully to avoid spurious cache misses. -// -// # Method Filtering -// -// Only GET and HEAD requests are processed. POST, PUT, DELETE, PATCH, -// and other methods bypass the middleware entirely (no buffering, no -// ETag computation) with zero allocations. -// -// # 304 Not Modified Response -// -// When returning 304, the middleware sends only the ETag header and any -// other response headers set by the handler. Content-Length is not included -// because the buffered response body (from which Content-Length would be -// derived) is discarded before the 304 is sent. This is RFC-compliant -- -// Content-Length is optional on 304 responses (RFC 9110 Section 8.6). -// -// # StreamWriter Incompatibility -// -// This middleware uses [celeris.Context.BufferResponse] which is incompatible -// with [celeris.Context.StreamWriter]. If the downstream handler uses -// StreamWriter, BufferResponse returns nil and the streamed response -// bypasses this middleware entirely. Use [Config].Skip to explicitly -// exclude streaming endpoints. -// -// # Full-Body Buffering -// -// The middleware buffers the entire response body in memory to compute -// the ETag checksum. For large responses, this increases memory usage. -// Use [Config].Skip or [Config].SkipPaths to bypass ETag processing -// for endpoints that return large payloads (file downloads, streaming): -// -// server.Use(etag.New(etag.Config{ -// SkipPaths: []string{"/download", "/export"}, -// })) -// -// # Middleware Ordering -// -// ETag should run INSIDE compression middleware (closer to the handler). -// This ensures the ETag is computed on the uncompressed body, so clients -// that re-request without Accept-Encoding still get a cache hit: -// -// server.Use(compress.New()) // outer: compresses the response -// server.Use(etag.New()) // inner: ETag on uncompressed body -// -// # Skipping -// -// Use [Config].Skip for dynamic skip logic or [Config].SkipPaths for -// path exclusions. SkipPaths uses exact path matching: -// -// server.Use(etag.New(etag.Config{ -// SkipPaths: []string{"/health", "/metrics"}, -// })) -// -// # Non-2xx and Empty Responses -// -// Responses with non-2xx status codes or empty bodies are flushed -// without ETag computation. Only successful responses with content -// participate in conditional caching. -// -// # Session Middleware Interaction -// -// When session middleware runs alongside ETag, 304 Not Modified responses -// still carry the Set-Cookie header from session refresh. This is correct -// behavior (sessions must be refreshed for security), but it prevents -// shared caches (CDN, reverse proxy) from caching 304 responses per -// RFC 7234 Section 3.1. Browser-level caching is unaffected. -// -// For static assets that benefit from shared-cache ETag optimization, -// consider using SkipPaths on the session middleware to exclude those paths. +// Package etag provides ETag generation and conditional-response middleware +// for celeris. +// +// [New] returns middleware that buffers the response body of GET and HEAD +// requests, computes an ETag validator, and sets the ETag header. When a +// request carries a matching If-None-Match header, it returns 304 Not +// Modified with no body. Other methods and non-2xx or empty responses pass +// through untouched. If the downstream handler already set an ETag header, +// that tag is reused rather than recomputed. +// +// By default the validator is a CRC-32 (IEEE) checksum of the body, emitted +// as a weak tag (W/"xxxxxxxx"). Configure behavior through [Config]: +// - Strong: emit strong tags ("xxxxxxxx") instead of weak. +// - HashFunc: supply a custom hash; it returns the opaque-tag, and the +// quotes and optional W/ prefix are added automatically. +// - Skip / SkipPaths: bypass buffering for dynamic conditions or for +// exact paths (e.g. large downloads, streaming endpoints). +// +// Because the body is buffered to compute the tag, etag should run INSIDE +// compression middleware so the validator is computed on the uncompressed +// body. +// +// # Documentation +// +// Full guides and examples: https://goceleris.dev/docs/middleware-content package etag diff --git a/middleware/healthcheck/doc.go b/middleware/healthcheck/doc.go index e612ae02..8c41324b 100644 --- a/middleware/healthcheck/doc.go +++ b/middleware/healthcheck/doc.go @@ -5,29 +5,20 @@ // paths (default "/livez", "/readyz", "/startupz") and returns a JSON // status response. Non-matching requests pass through with zero overhead. // -// Basic usage (all probes return 200): +// Key exported symbols: [New] constructs the handler; [Config] controls probe +// paths, per-probe [Checker] functions, the [Config].Skip bypass predicate, +// and [Config].CheckerTimeout (default [DefaultCheckerTimeout], 5 s). Set +// [Config].CheckerTimeout to [FastPathTimeout] for trivial checkers that +// cannot block (runs inline, no goroutine overhead). Panicking checkers are +// recovered and return 503. // -// server.Use(healthcheck.New()) -// -// Custom readiness check: -// -// server.Use(healthcheck.New(healthcheck.Config{ -// ReadyChecker: func(_ *celeris.Context) bool { -// return db.Ping() == nil -// }, -// })) -// -// Each checker is guarded by [Config].CheckerTimeout (default 5s); if it -// does not return in time the probe responds 503. Set CheckerTimeout to -// [FastPathTimeout] for trivial checkers that cannot block (runs inline, -// no goroutine overhead). Panicking checkers are recovered and return 503. -// -// Setting a probe path to "" disables that probe. [Config].Skip bypasses -// the middleware before path matching. -// -// Invalid paths (missing leading '/') or overlapping enabled paths cause -// a panic at initialization. These are programming errors caught early. +// Setting a probe path to "" disables that probe. Invalid paths (missing +// leading '/') or overlapping enabled paths cause a panic at initialization. // // Response format: 200 {"status":"ok"} / 503 {"status":"unavailable"}. // HEAD requests return the status code with no body. +// +// # Documentation +// +// Full guides and examples: https://goceleris.dev/docs/middleware package healthcheck diff --git a/middleware/idempotency/doc.go b/middleware/idempotency/doc.go index ca705976..cf76943a 100644 --- a/middleware/idempotency/doc.go +++ b/middleware/idempotency/doc.go @@ -1,42 +1,21 @@ -// Package idempotency implements the HTTP Idempotency-Key request pattern. -// -// # Semantics -// -// The middleware looks up the header named by [Config.KeyHeader] -// (default "Idempotency-Key") and: -// -// 1. Validates the key is 1..MaxKeyLength printable ASCII. -// 2. Attempts to atomically acquire a lock via [store.SetNXer]. -// 3. On acquisition, runs the handler and stores the captured -// response under the key with [Config.TTL] expiry. The lock -// entry is replaced by the completed entry atomically. -// 4. On contention with an in-flight duplicate, returns 409 Conflict -// via [Config.OnConflict] (no wait-and-retry). -// 5. On a subsequent request that finds a completed entry, replays -// the stored response without running the handler. -// -// # Body hash -// -// [Config.BodyHash] enables SHA-256 hashing of the request body (up -// to [Config.MaxBodyBytes]) and compares it to the stored hash on -// replay. Mismatches return 422 Unprocessable Entity — catching -// clients that reuse a key with different payloads, which the IETF -// draft recommends rejecting. Disabled by default because hashing -// large bodies costs memory. -// -// # Store requirements -// -// The store must implement both [store.KV] and [store.SetNXer]. The -// default [NewMemoryStore] is in-memory and sharded, suitable for -// single-instance deployments. For multi-instance deployments, wire -// a [middleware/session/redisstore.Store] — it satisfies both -// interfaces. (Wrap with [store.Prefixed] to namespace alongside -// session data.) -// -// # Lock expiry -// -// A crashed handler leaves the lock entry behind. Once -// [Config.LockTimeout] passes, the key becomes acquirable again. -// Choose LockTimeout ≥ the worst-case handler latency plus network -// margin to avoid a second execution of a still-running request. +// Package idempotency implements the HTTP Idempotency-Key request pattern for Celeris. +// +// [New] returns a middleware that deduplicates retried writes. When a request +// carries the header named by [Config.KeyHeader] (default "Idempotency-Key"), +// the middleware acquires an atomic lock via [KVStore], runs the handler once, +// persists the response, and replays it on subsequent requests with the same +// key. Concurrent duplicates return 409 Conflict (overridable via +// [Config.OnConflict]). Requests without the key header pass through unchanged. +// +// Key types: [Config] controls all behaviour; [KVStore] is the store interface +// ([store.KV] + [store.SetNXer]); [NewMemoryStore] provides a sharded +// in-memory default suitable for single-instance deployments. For +// multi-instance deployments, supply a Redis-backed store and namespace it +// with [store.Prefixed]. Set [Config.BodyHash] to detect key reuse with +// different payloads (returns 422 on mismatch). [ErrStoreMissingSetNX] is +// returned by [New] when a provided store lacks atomic SetNX support. +// +// # Documentation +// +// Full guides and examples: https://goceleris.dev/docs/middleware-traffic package idempotency diff --git a/middleware/jwt/doc.go b/middleware/jwt/doc.go index 36c4077d..55fbf115 100644 --- a/middleware/jwt/doc.go +++ b/middleware/jwt/doc.go @@ -22,48 +22,19 @@ // JWKSRefresh: 30 * time.Minute, // })) // -// # Retrieving Token and Claims +// Retrieve the validated token and claims in a handler: // // token := jwt.TokenFromContext(c) // claims, ok := jwt.ClaimsFromContext[jwt.MapClaims](c) // -// # Token Lookup +// Key configuration options: [Config].TokenLookup controls where the token is +// extracted (comma-separated "source:name[:prefix]" pairs tried in order; +// default "header:Authorization:Bearer "). [Config].ClaimsFactory creates a +// fresh [Claims] instance per request for custom struct types. [Config].Skip +// and [Config].SkipPaths bypass the middleware dynamically or by exact path. +// Use [SignToken] to create signed tokens for testing or token issuance. // -// [Config].TokenLookup supports comma-separated sources tried in order. -// Format: "source:name[:prefix]". Sources: header, query, cookie, form, param. +// # Documentation // -// # Custom Claims -// -// Use [Config].ClaimsFactory for custom struct claims types: -// -// jwt.New(jwt.Config{ -// SigningKey: secret, -// ClaimsFactory: func() jwt.Claims { return &MyClaims{} }, -// }) -// -// # Creating Tokens -// -// token, err := jwt.SignToken(jwt.SigningMethodHS256, jwt.MapClaims{ -// "sub": "user-123", -// "exp": float64(time.Now().Add(time.Hour).Unix()), -// }, []byte("secret")) -// -// # Security Best Practices -// -// Always set "exp" on issued tokens. Use "aud" with [WithAudience] to -// prevent token confusion. Prefer asymmetric algorithms (RS256, ES256) in -// multi-service deployments. Store keys in environment variables or a -// secrets manager. Serve JWKS endpoints over HTTPS. -// -// # Algorithm-Key-Type Binding -// -// - HS256/HS384/HS512: []byte -// - RS256/RS384/RS512/PS256/PS384/PS512: *rsa.PublicKey / *rsa.PrivateKey -// - ES256/ES384/ES512: *ecdsa.PublicKey / *ecdsa.PrivateKey -// - EdDSA: ed25519.PublicKey / ed25519.PrivateKey -// -// # Skipping -// -// Set [Config].Skip to bypass dynamically, or [Config].SkipPaths for -// exact-match path exclusions. +// Full guides and examples: https://goceleris.dev/docs/middleware-auth package jwt diff --git a/middleware/jwt/jwt.go b/middleware/jwt/jwt.go index 20a532f5..590cd752 100644 --- a/middleware/jwt/jwt.go +++ b/middleware/jwt/jwt.go @@ -16,19 +16,28 @@ import ( // errors.Is checks work (e.g. mixed jwt+keyauth stacks). var ErrUnauthorized = celeris.ErrUnauthorized +// unauthorizedCause carries a jwt-specific detail message while chaining to +// celeris.ErrUnauthorized. It lets errors.Is(err, celeris.ErrUnauthorized) match +// every jwt rejection — consistent with keyauth and basicauth — without changing +// the text surfaced by HTTPError.Error. +type unauthorizedCause struct{ msg string } + +func (u *unauthorizedCause) Error() string { return u.msg } +func (u *unauthorizedCause) Unwrap() error { return celeris.ErrUnauthorized } + // ErrTokenMissing is returned when no token is found in the request. -var ErrTokenMissing = &celeris.HTTPError{Code: 401, Message: "Unauthorized", Err: errors.New("jwt: missing or malformed token")} +var ErrTokenMissing = &celeris.HTTPError{Code: 401, Message: "Unauthorized", Err: &unauthorizedCause{"jwt: missing or malformed token"}} // ErrTokenInvalid is returned when the token fails validation for reasons // other than expiration or malformation (e.g., bad signature, unknown kid). -var ErrTokenInvalid = &celeris.HTTPError{Code: 401, Message: "Unauthorized", Err: errors.New("jwt: invalid or expired token")} +var ErrTokenInvalid = &celeris.HTTPError{Code: 401, Message: "Unauthorized", Err: &unauthorizedCause{"jwt: invalid or expired token"}} // ErrJWTExpired is returned when the token's exp claim is in the past. -var ErrJWTExpired = &celeris.HTTPError{Code: 401, Message: "Unauthorized", Err: errors.New("jwt: token has expired")} +var ErrJWTExpired = &celeris.HTTPError{Code: 401, Message: "Unauthorized", Err: &unauthorizedCause{"jwt: token has expired"}} // ErrJWTMalformed is returned when the token cannot be parsed (bad // encoding, wrong number of segments, etc.). -var ErrJWTMalformed = &celeris.HTTPError{Code: 401, Message: "Unauthorized", Err: errors.New("jwt: token is malformed")} +var ErrJWTMalformed = &celeris.HTTPError{Code: 401, Message: "Unauthorized", Err: &unauthorizedCause{"jwt: token is malformed"}} // New creates a JWT authentication middleware with the given config. func New(config ...Config) celeris.HandlerFunc { diff --git a/middleware/jwt/unauthorized_match_test.go b/middleware/jwt/unauthorized_match_test.go new file mode 100644 index 00000000..c61060a1 --- /dev/null +++ b/middleware/jwt/unauthorized_match_test.go @@ -0,0 +1,39 @@ +package jwt + +import ( + "errors" + "testing" + + "github.com/goceleris/celeris" +) + +// The jwt sentinels must satisfy errors.Is(err, celeris.ErrUnauthorized) so a +// mixed auth stack (jwt + keyauth + basicauth) can match every 401 with one +// check, as documented on celeris.ErrUnauthorized. +func TestSentinelsMatchErrUnauthorized(t *testing.T) { + for _, e := range []error{ErrTokenMissing, ErrTokenInvalid, ErrJWTExpired, ErrJWTMalformed} { + if !errors.Is(e, celeris.ErrUnauthorized) { + t.Errorf("errors.Is(%v, celeris.ErrUnauthorized) = false, want true", e) + } + // The classifiedError wrapper (used on the real reject paths) must match too. + ce := &classifiedError{outer: e, inner: errors.New("detail")} + if !errors.Is(ce, celeris.ErrUnauthorized) { + t.Errorf("errors.Is(classifiedError{%v}, celeris.ErrUnauthorized) = false, want true", e) + } + } +} + +// Wrapping ErrUnauthorized must not change the surfaced error text. +func TestSentinelMessagesUnchanged(t *testing.T) { + cases := map[*celeris.HTTPError]string{ + ErrTokenMissing: "code=401, message=Unauthorized, err=jwt: missing or malformed token", + ErrTokenInvalid: "code=401, message=Unauthorized, err=jwt: invalid or expired token", + ErrJWTExpired: "code=401, message=Unauthorized, err=jwt: token has expired", + ErrJWTMalformed: "code=401, message=Unauthorized, err=jwt: token is malformed", + } + for e, want := range cases { + if got := e.Error(); got != want { + t.Errorf("Error() = %q, want %q", got, want) + } + } +} diff --git a/middleware/keyauth/doc.go b/middleware/keyauth/doc.go index 7c7aa1be..246bb939 100644 --- a/middleware/keyauth/doc.go +++ b/middleware/keyauth/doc.go @@ -2,46 +2,30 @@ // // The middleware extracts an API key from a configurable source (header, // query parameter, cookie, form field, or URL parameter), validates it via -// a user-supplied function, and stores the authenticated key in the context -// store under [ContextKey] ("keyauth_key"). Failed authentication returns -// 401 with a WWW-Authenticate header. +// a user-supplied [Config].Validator function, and stores the authenticated +// key in the context store under [ContextKey] ("keyauth_key"). Failed +// authentication returns 401 with a WWW-Authenticate header. +// +// Key exported symbols: +// - [New] — constructs the middleware handler from a [Config]. +// - [Config] — controls key lookup, validation, skip rules, realm, auth +// scheme, RFC 6750 challenge parameters, and success/error callbacks. +// - [StaticKeys] — constant-time validator for a fixed set of API keys. +// - [KeyFromContext] — retrieves the validated key from a request context. +// - [ErrMissingKey], [ErrUnauthorized] — sentinel 401 errors usable with +// errors.Is. // // [Config].Validator is required; omitting it panics at initialization. +// [Config].KeyLookup supports comma-separated fallback sources, e.g. +// "header:Authorization:Bearer ,query:api_key". Set [Config].Skip or +// [Config].SkipPaths to bypass the middleware selectively; OPTIONS requests +// are always skipped to allow CORS preflight through. // -// Simple usage with static keys (constant-time comparison): +// Security note: keys extracted from query strings appear in access logs, +// browser history, and Referer headers. Prefer header-based lookup for +// sensitive environments. // -// server.Use(keyauth.New(keyauth.Config{ -// Validator: keyauth.StaticKeys("key-1", "key-2"), -// })) +// # Documentation // -// Custom validator with context access: -// -// server.Use(keyauth.New(keyauth.Config{ -// Validator: func(c *celeris.Context, key string) (bool, error) { -// return checkDB(c, key) -// }, -// KeyLookup: "query:api_key", -// })) -// -// Multiple sources can be comma-separated for fallback: -// -// KeyLookup: "header:Authorization:Bearer ,query:api_key" -// -// Use [KeyFromContext] to retrieve the authenticated key downstream. -// -// [Config].ChallengeParams controls RFC 6750 parameters in the -// WWW-Authenticate header (error, error_description, error_uri, scope). -// -// [StaticKeys] uses constant-time comparison via [crypto/subtle] to -// prevent timing side-channels. -// -// [ErrUnauthorized] and [ErrMissingKey] are exported sentinel errors -// (both 401) usable with errors.Is for upstream error handling. -// -// Set [Config].Skip to bypass the middleware dynamically, or -// [Config].SkipPaths for exact-match path exclusions. -// -// Security: API keys in query strings (`query:api_key`) are logged in -// access logs, browser history, and Referer headers. Prefer header-based -// key lookup for sensitive environments. +// Full guides and examples: https://goceleris.dev/docs/middleware-auth package keyauth diff --git a/middleware/logger/doc.go b/middleware/logger/doc.go index 07d17806..8afdd8c7 100644 --- a/middleware/logger/doc.go +++ b/middleware/logger/doc.go @@ -1,82 +1,28 @@ // Package logger provides HTTP request logging middleware for celeris. // -// The middleware logs each request with configurable fields including -// method, path, status, latency, and bytes written. It supports -// both standard slog handlers and the zero-alloc [FastHandler]. -// -// Basic usage with the default slog output: -// -// server.Use(logger.New()) -// -// Using the zero-alloc FastHandler with color output: -// -// mw := logger.New(logger.Config{ -// Output: slog.New(logger.NewFastHandler(os.Stderr, &logger.FastHandlerOptions{ -// Color: true, -// })), -// }) -// server.Use(mw) -// -// # Body Capture -// -// Set [Config].CaptureRequestBody and/or [Config].CaptureResponseBody to -// log request and response bodies. Bodies are truncated to -// [Config].MaxCaptureBytes (default 4096). -// -// # Sensitive Header Redaction -// -// [Config].SensitiveHeaders lists header names whose values are redacted. -// When nil, [DefaultSensitiveHeaders] is used. Set to an empty slice to -// disable all redaction. -// -// # Request ID Integration -// -// The middleware reads the request ID from the context store -// (key "request_id") first, falling back to the x-request-id header. -// -// # Predefined Configurations -// -// [CLFConfig] returns a Config for Common Log Format style output. -// [JSONConfig] returns a Config for structured JSON output. -// Both return a [Config] value that can be further customized: -// -// cfg := logger.CLFConfig() -// cfg.SkipPaths = []string{"/health"} -// server.Use(logger.New(cfg)) -// -// # Design Rationale -// -// All output is structured through Go's [log/slog] package. No template -// strings are provided. The [Config].Fields callback supplies arbitrary -// extensibility. [FastHandler] formats directly into pooled byte buffers, -// avoiding fmt.Sprintf and time.Format entirely. -// -// # Query Parameter Security -// -// Security: LogQueryParams logs raw query strings which may contain -// sensitive values (OAuth tokens, API keys, session identifiers). -// Consider using SkipPaths for sensitive endpoints or implementing a -// custom Fields function that redacts sensitive query parameters. -// -// # Skipping -// -// Set [Config].Skip to bypass dynamically, or [Config].SkipPaths for -// exact-match path exclusions. -// -// # Middleware Order -// -// Register after requestid for request ID inclusion in logs. -// -// # Reverse Proxy Integration -// -// When running behind a reverse proxy, install the proxy middleware via -// Server.Pre() so that Logger sees the real client IP: -// -// server.Pre(proxy.New(proxy.Config{ -// TrustedProxies: []string{"10.0.0.0/8"}, -// })) -// server.Use(logger.New()) // now logs the real client IP -// -// Without proxy middleware, Logger records the reverse proxy's IP address, -// not the end user's. +// [New] returns a [celeris.HandlerFunc] that logs each request with method, +// path, status, latency, bytes written, client IP, and request ID. All output +// is routed through Go's [log/slog] package; supply any slog.Handler via +// [Config].Output, or use [NewFastHandler] for zero-alloc pooled-buffer output. +// +// Two preset constructors cover the most common cases: [CLFConfig] produces +// Common Log Format style output; [JSONConfig] produces structured JSON via +// slog.JSONHandler. Both return a [Config] that can be further customised before +// passing to [New]. +// +// Notable Config fields: +// - CaptureRequestBody / CaptureResponseBody — log bodies, truncated to MaxCaptureBytes (default 4096). +// - SensitiveHeaders — header values to redact; nil uses [DefaultSensitiveHeaders]. +// - LogFormValues / SensitiveFormFields — log form fields with optional redaction; use [DefaultSensitiveFormFields] as a safe starting list. +// - Skip / SkipPaths — bypass the middleware dynamically or for exact-match paths. +// - Fields / Done — callbacks for custom attributes and post-log hooks. +// - LogContextKeys — emit arbitrary context-store values as "ctx." attributes. +// +// Register after the requestid middleware so request IDs are available in every +// log entry. When running behind a reverse proxy, install the proxy middleware +// via Server.Pre() before logger.New() so the real client IP is recorded. +// +// # Documentation +// +// Full guides and examples: https://goceleris.dev/docs/observability package logger diff --git a/middleware/methodoverride/doc.go b/middleware/methodoverride/doc.go index 617b6156..5fe7aa6b 100644 --- a/middleware/methodoverride/doc.go +++ b/middleware/methodoverride/doc.go @@ -1,77 +1,30 @@ -// Package methodoverride provides HTTP method override middleware for -// celeris. -// -// HTML forms can only submit GET and POST requests. This middleware allows -// clients to tunnel PUT, PATCH, DELETE, and other methods through a POST -// request by specifying the intended method in a header or form field. -// -// # IMPORTANT: Use Server.Pre(), Not Server.Use() -// -// This middleware MUST be registered via [celeris.Server.Pre], not -// [celeris.Server.Use]. With Server.Use, the router has already matched -// the request based on the original method, making the override ineffective. -// Using Server.Use will silently produce wrong routing behavior. -// -// Register the middleware with [celeris.Server.Pre] so the method is -// rewritten before routing: -// -// s := celeris.New() -// s.Pre(methodoverride.New()) -// -// # Override Sources -// -// By default, the middleware checks the form field [DefaultFormField] -// ("_method") first, then the header [DefaultHeader] -// ("X-HTTP-Method-Override"). Only POST requests are eligible for override -// (configurable via [Config].AllowedMethods). -// -// # Custom Getters -// -// Use [HeaderGetter] to read from a specific header only: -// -// s.Pre(methodoverride.New(methodoverride.Config{ -// Getter: methodoverride.HeaderGetter("X-Method"), -// })) -// -// Use [FormFieldGetter] to read from a specific form field only: -// -// s.Pre(methodoverride.New(methodoverride.Config{ -// Getter: methodoverride.FormFieldGetter("_http_method"), -// })) -// -// Use [FormThenHeaderGetter] to check a custom form field first, then a -// custom header (same order as the default getter but with custom names): -// -// s.Pre(methodoverride.New(methodoverride.Config{ -// Getter: methodoverride.FormThenHeaderGetter("_method", "X-HTTP-Method"), -// })) -// -// [QueryGetter] reads from a URL query parameter. This is provided for -// parity with Echo but carries security risks: query parameters are -// embeddable in links and images, potentially enabling cross-site method -// override attacks. Use only when the security implications are understood. -// -// # Target Methods -// -// By default, only PUT, DELETE, and PATCH are valid override targets -// (configurable via [Config].TargetMethods). Override values not in this -// list are silently ignored, preventing clients from overriding to -// arbitrary methods such as CONNECT or TRACE. -// -// # Validation -// -// [Config].AllowedMethods and [Config].TargetMethods must not contain empty -// or whitespace-only strings. The middleware panics at initialization if -// this constraint is violated. -// -// # Skipping -// -// Set [Config].Skip to bypass the middleware dynamically, or -// [Config].SkipPaths for exact-match path exclusions. -// -// # CSRF Middleware Interaction -// -// Method override changes the request method before CSRF middleware runs. -// Ensure that overridden methods (PUT, DELETE, PATCH) are NOT in the CSRF -// middleware's SafeMethods list. See middleware/csrf documentation for details. +// Package methodoverride provides HTTP method override middleware for celeris. +// +// HTML forms can only submit GET and POST requests. This middleware lets +// clients tunnel PUT, PATCH, DELETE, and other methods through a POST request +// by specifying the intended method via a form field or header. +// +// Register with [celeris.Server.Pre] (not Server.Use) so the method is +// rewritten before routing occurs. [New] accepts an optional [Config] to +// customise behaviour: +// +// - [Config].AllowedMethods — original methods eligible for override +// (default: POST). +// - [Config].TargetMethods — valid override targets (default: PUT, DELETE, +// PATCH); values outside this set are silently ignored. +// - [Config].Getter — function that extracts the override value; built-in +// helpers are [HeaderGetter], [FormFieldGetter], [FormThenHeaderGetter], +// and [QueryGetter] (note: QueryGetter carries CSRF risk via embeddable +// URLs). +// - [Config].Skip / [Config].SkipPaths — skip the middleware per-request or +// by exact path. +// +// The default getter checks form field [DefaultFormField] ("_method") first, +// then header [DefaultHeader] ("X-HTTP-Method-Override"). [Config].AllowedMethods +// and [Config].TargetMethods must not contain empty or whitespace-only strings; +// the middleware panics at initialisation if this constraint is violated. +// +// # Documentation +// +// Full guides and examples: https://goceleris.dev/docs/middleware-routing-helpers package methodoverride diff --git a/middleware/metrics/doc.go b/middleware/metrics/doc.go index be8332ef..16791ade 100644 --- a/middleware/metrics/doc.go +++ b/middleware/metrics/doc.go @@ -4,11 +4,12 @@ // and exposes them in Prometheus text exposition format at a configurable // endpoint (default "/metrics"). // -// Basic usage: +// Register with default settings: // // server.Use(metrics.New()) // -// Custom configuration: +// Or supply a [Config] to customise the endpoint path, namespace, subsystem, +// histogram buckets, label extensions, or the backing [prometheus.Registry]: // // server.Use(metrics.New(metrics.Config{ // Path: "/prom", @@ -32,19 +33,9 @@ // - {ns}_{sub}_active_requests (Gauge): in-flight requests // // The subsystem segment is omitted when [Config].Subsystem is empty. -// -// # Custom Labels -// -// Use [Config].LabelFuncs to add custom label dimensions. Functions are -// called after c.Next() returns: -// -// server.Use(metrics.New(metrics.Config{ -// LabelFuncs: map[string]func(*celeris.Context) string{ -// "region": func(c *celeris.Context) string { -// return c.Header("x-region") -// }, -// }, -// })) +// Base labels on all metrics are method, path, and status. Add custom +// dimensions via [Config].LabelFuncs; keys must not conflict with the +// three reserved base labels. // // # Cardinality Protection // @@ -53,10 +44,7 @@ // // # Histogram Buckets // -// [DefaultBuckets] provides fine-grained latency boundaries: -// -// []float64{0.0005, 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 5} -// +// [DefaultBuckets] provides fine-grained sub-millisecond latency boundaries. // Override with [Config].Buckets for duration histograms or // [Config].SizeBuckets for request/response size histograms. // @@ -73,4 +61,8 @@ // (metrics before compress), this records the uncompressed application-level // size. If metrics runs after compress, it records the compressed network-level // size. Be aware of this when interpreting response size dashboards. +// +// # Documentation +// +// Full guides and examples: https://goceleris.dev/docs/observability package metrics diff --git a/middleware/otel/doc.go b/middleware/otel/doc.go index 2827f0cd..d9da26e9 100644 --- a/middleware/otel/doc.go +++ b/middleware/otel/doc.go @@ -1,45 +1,27 @@ // Package otel provides OpenTelemetry tracing and metrics middleware // for celeris. // -// The middleware creates a server span per request, propagates context -// via configured propagators, and records request duration and active -// request count as OTel metrics. -// -// Basic usage with global providers: -// -// server.Use(otel.New()) -// -// Explicit providers: -// -// server.Use(otel.New(otel.Config{ -// TracerProvider: tp, -// MeterProvider: mp, -// })) -// -// Downstream handlers can access the span via [SpanFromContext]: -// -// span := otel.SpanFromContext(c) -// -// Spans are named "METHOD /route/pattern" by default; override with +// [New] returns a [celeris.HandlerFunc] that creates a server span per +// request, propagates context via configured propagators, and records +// request duration and active request count as OTel metrics. By default +// it reads the global TracerProvider, MeterProvider, and TextMapPropagator; +// supply a [Config] to use explicit providers. +// +// Downstream handlers retrieve the active span with [SpanFromContext]. +// Span names default to "METHOD /route/pattern"; override with // [Config].SpanNameFormatter. Attributes follow OTel semconv v1.32.0. // -// PII controls: client.address is opt-in ([Config].CollectClientIP); -// user_agent.original is opt-out ([Config].CollectUserAgent). -// -// Filtering: use [Config].Skip, [Config].SkipPaths, or [Config].Filter -// to exclude endpoints from tracing. -// // Metrics recorded: http.server.request.duration (s), // http.server.active_requests, http.server.request.body.size (By), // http.server.response.body.size (By). Set [Config].DisableMetrics for -// tracing-only mode. Use [Config].CustomAttributes and -// [Config].CustomMetricAttributes for per-request attribute injection. +// tracing-only mode. +// +// PII controls: client.address is opt-in ([Config].CollectClientIP); +// user_agent.original is opt-out ([Config].CollectUserAgent). +// Use [Config].Skip, [Config].SkipPaths, or [Config].Filter to exclude +// endpoints from tracing. // -// # Response Size and Compression +// # Documentation // -// The http.response.body.size metric records c.BytesWritten() at the point -// the OTel middleware runs. With the recommended ordering (otel before -// compress), this reflects uncompressed application-level sizes. Changing -// the middleware order to place otel after compress would record compressed -// sizes instead. +// Full guides and examples: https://goceleris.dev/docs/observability package otel diff --git a/middleware/pprof/doc.go b/middleware/pprof/doc.go index 083edfc3..5ec7525d 100644 --- a/middleware/pprof/doc.go +++ b/middleware/pprof/doc.go @@ -5,54 +5,17 @@ // (default "/debug/pprof") and dispatches to the matching pprof handler. // Non-matching requests pass through to the next handler with zero overhead. // -// # Security -// -// By default, access is restricted to loopback addresses (127.0.0.1, ::1) -// via [Config].AuthFunc. This uses the raw TCP peer address, NOT -// X-Forwarded-For. Behind a reverse proxy, set AuthFunc to a scheme that -// does not rely on RemoteAddr (e.g., shared secret header). -// -// Profiling endpoints expose sensitive runtime internals. Never expose them -// publicly in production without proper authentication. -// -// # Endpoints -// -// All endpoints are relative to the configured prefix: -// -// {prefix}/ — index page listing available profiles -// {prefix}/cmdline — command-line arguments -// {prefix}/profile — CPU profile (accepts ?seconds=N) -// {prefix}/symbol — symbol lookup -// {prefix}/trace — execution trace (accepts ?seconds=N) -// {prefix}/allocs — allocation profile -// {prefix}/block — block profile -// {prefix}/goroutine — goroutine stacks -// {prefix}/heap — heap profile -// {prefix}/mutex — mutex contention profile -// {prefix}/threadcreate — thread creation profile -// -// # Ordering -// -// Place pprof after the debug middleware in the middleware chain. Since pprof -// intercepts by path prefix, it can be installed at any position, but placing -// it after debug avoids shadowing debug endpoints when both share the -// /debug/ prefix namespace. -// -// # Basic usage +// Use [New] with an optional [Config] to mount the profiling endpoints: // // server.Use(pprof.New()) // -// # Custom AuthFunc -// -// server.Use(pprof.New(pprof.Config{ -// AuthFunc: func(c *celeris.Context) bool { -// return c.Header("x-pprof-token") == os.Getenv("PPROF_TOKEN") -// }, -// })) +// [Config].Prefix controls the URL prefix (default "/debug/pprof"). +// [Config].AuthFunc gates access; the default restricts to loopback addresses +// (127.0.0.1 and ::1) using the raw TCP peer address — set a custom AuthFunc +// when running behind a reverse proxy. [Config].Skip and [Config].SkipPaths +// provide request-level bypass logic. // -// # Skipping +// # Documentation // -// Use [Config].Skip for dynamic skip logic or [Config].SkipPaths for -// exact-match path exclusions. Skipped requests call c.Next() without -// invoking the auth check or serving profiles. +// Full guides and examples: https://goceleris.dev/docs/observability package pprof diff --git a/middleware/protobuf/doc.go b/middleware/protobuf/doc.go index 9dedd889..7ea31d4a 100644 --- a/middleware/protobuf/doc.go +++ b/middleware/protobuf/doc.go @@ -1,50 +1,26 @@ // Package protobuf provides Protocol Buffers serialization for celeris. // -// Protocol Buffers was removed from the celeris core to avoid forcing -// google.golang.org/protobuf as a transitive dependency. This package -// provides equivalent functionality as standalone functions. -// -// # Basic Usage -// -// Write a protobuf response: -// -// protobuf.Write(c, 200, &myProto) -// -// Parse a protobuf request: -// -// var msg pb.MyMessage -// if err := protobuf.BindProtoBuf(c, &msg); err != nil { -// return err -// } -// -// Auto-detect content type: -// -// var msg pb.MyMessage -// if err := protobuf.Bind(c, &msg); err != nil { -// return err -// } -// -// # Content Negotiation -// -// Use [Respond] to serve protobuf or JSON based on the Accept header: -// -// protobuf.Respond(c, 200, &myProto, myJSONStruct) -// -// # Middleware with Custom Options -// -// Install the middleware for custom marshal/unmarshal options: -// -// s.Use(protobuf.New(protobuf.Config{ -// MarshalOptions: proto.MarshalOptions{Deterministic: true}, -// })) -// -// Then use [FromContext] in handlers: -// -// pb := protobuf.FromContext(c) -// pb.Write(200, &myProto) -// -// # Content Types -// -// Both "application/x-protobuf" (primary) and "application/protobuf" are -// recognized. Responses use "application/x-protobuf". +// Protocol Buffers support was split from the celeris core to avoid pulling +// google.golang.org/protobuf as a transitive dependency. This package provides +// equivalent functionality as standalone helpers and an optional middleware. +// +// Key exports: +// - [Write] — marshal a proto.Message and write it with status code. +// - [BindProtoBuf] — read the request body and unmarshal it as protobuf. +// - [Bind] — like BindProtoBuf but first checks the Content-Type header, +// returning [ErrNotProtoBuf] when it is not a protobuf content type. +// - [Respond] — content-negotiate between protobuf and JSON using the +// Accept header, honoring q=0 exclusions per RFC 7231 §5.3.1. +// - [New] / [Config] — optional middleware that stashes marshal/unmarshal +// options in the request context for retrieval via [FromContext]. +// - [PoolEvictions] — monotonic counter for buffers discarded from the +// internal marshal pool; wire into metrics to detect large messages. +// +// Both "application/x-protobuf" ([ContentType]) and "application/protobuf" +// ([ContentTypeAlt]) are accepted on inbound requests. Responses written by +// [Write] always use "application/x-protobuf". +// +// # Documentation +// +// Full guides and examples: https://goceleris.dev/docs/middleware-content package protobuf diff --git a/middleware/proxy/doc.go b/middleware/proxy/doc.go index 92640ae6..4716192d 100644 --- a/middleware/proxy/doc.go +++ b/middleware/proxy/doc.go @@ -1,121 +1,32 @@ -// Package proxy extracts real client IP, scheme, and host from trusted +// Package proxy extracts the real client IP, scheme, and host from trusted // reverse proxy headers. // // When a celeris server sits behind a load balancer or reverse proxy -// (e.g. Nginx, Cloudflare, AWS ALB), the TCP peer address is the proxy, -// not the end user. This middleware inspects X-Forwarded-For, X-Real-Ip, -// X-Forwarded-Proto, and X-Forwarded-Host headers -- but only when the -// immediate peer is in the configured [Config].TrustedProxies list. +// (e.g. Nginx, Cloudflare, AWS ALB), the TCP peer is the proxy, not the end +// user. [New] returns a middleware that inspects X-Forwarded-For, X-Real-Ip, +// X-Forwarded-Proto, and X-Forwarded-Host -- but only when the immediate peer +// is in the configured [Config.TrustedProxies] list. An empty TrustedProxies +// list is the safe default: the middleware becomes a no-op and trusts nothing, +// preventing IP spoofing. There is no "trust all" toggle; specify CIDRs (or +// "0.0.0.0/0") explicitly. Invalid entries panic at [New]. // -// # Security Model +// Run it via [celeris.Server.Pre] so the overrides take effect before routing +// and before any middleware reads ClientIP, Scheme, or Host: // -// An empty TrustedProxies list is the safe default: the middleware becomes -// a no-op and never trusts any forwarded header. This prevents IP spoofing -// when the server is exposed directly to the internet. -// -// There is no TrustAllProxies option. Trusting all proxies defeats the -// purpose of the right-to-left walk and opens the door to trivial -// IP spoofing. If you need to trust all proxies in a controlled -// environment, specify the CIDR explicitly (e.g. "0.0.0.0/0"). -// -// # Disable* Convention -// -// [Config] uses the Disable* pattern for ForwardedProto and ForwardedHost. -// The Go zero value (false) means enabled, so a minimal Config literal -// automatically processes both headers: -// -// server.Pre(proxy.New(proxy.Config{ -// TrustedProxies: []string{"10.0.0.0/8"}, -// })) -// -// To opt out of one or both, set the corresponding Disable* field: -// -// server.Pre(proxy.New(proxy.Config{ -// TrustedProxies: []string{"10.0.0.0/8"}, -// DisableForwardedProto: true, -// })) -// -// # TrustedProxies -// -// Accepts CIDR notation ("10.0.0.0/8") and bare IPs ("10.0.0.1"). -// Bare IPs are expanded to /32 (IPv4) or /128 (IPv6) at init time. -// Invalid entries cause a panic so misconfigurations surface immediately. -// -// Common provider CIDR ranges: -// - Cloudflare: https://www.cloudflare.com/ips/ -// - AWS CloudFront: https://ip-ranges.amazonaws.com/ip-ranges.json -// - GCP: https://cloud.google.com/load-balancing/docs/https#target-proxies -// -// # TrustedHeaders -// -// By default, "x-forwarded-for" and "x-real-ip" are inspected. These two -// headers have built-in logic: XFF is walked right-to-left, and X-Real-IP -// is validated with net/netip.ParseAddr. -// -// Any other header name added to TrustedHeaders (e.g. "cf-connecting-ip", -// "true-client-ip") is treated as a single-value IP header. The value is -// parsed with netip.ParseAddr and used only if valid. Custom headers are -// checked after XFF and X-Real-IP, in order of appearance. -// -// # X-Forwarded-For Walk -// -// The middleware walks the X-Forwarded-For chain right-to-left, skipping -// entries that match a trusted network. The first untrusted IP is taken as -// the real client IP. This algorithm is resilient against left-side -// spoofing because attacker-controlled entries appear on the left, while -// each trusted proxy appends to the right. -// -// If a malformed (non-IP) entry is encountered during the walk, traversal -// stops and no client IP override is applied, falling back to the peer -// address. -// -// # X-Forwarded-Host Validation -// -// When X-Forwarded-Host processing is enabled (the default), the value is validated -// before being applied. Values containing \r, \n, \x00, /, \, ?, #, or @ -// are rejected to prevent header injection and path traversal. Hosts -// exceeding 253 bytes (the DNS maximum per RFC 1035) are also rejected. -// -// # IPv4-Mapped IPv6 -// -// IPv4-mapped IPv6 addresses (::ffff:10.0.0.1) are normalized via -// netip.Addr.Unmap so that a trusted network specified as 10.0.0.0/8 -// correctly matches the mapped form. -// -// # ForwardedProto and ForwardedHost -// -// By default (Disable* fields false), X-Forwarded-Proto overrides -// [celeris.Context.Scheme] and X-Forwarded-Host overrides -// [celeris.Context.Host]. Only "http" and "https" are accepted for proto; -// other values are silently ignored. -// -// # RFC 7239 (Forwarded) -// -// The standard Forwarded header (RFC 7239) is not currently implemented. -// Most real-world proxies emit X-Forwarded-For, and the middleware focuses -// on that de facto standard. RFC 7239 support may be added in a future -// release. -// -// # Runs via Server.Pre -// -// This middleware is designed to run via [celeris.Server.Pre] so that the -// overrides take effect before routing and before any downstream middleware -// reads ClientIP, Scheme, or Host. -// -// s := celeris.New(celeris.Config{Addr: ":8080"}) // s.Pre(proxy.New(proxy.Config{ // TrustedProxies: []string{"10.0.0.0/8", "172.16.0.0/12"}, // })) // -// # Downstream Middleware -// -// Several middleware depend on proxy for correct values: -// - logger: logs c.ClientIP() — without proxy, logs the proxy's IP -// - ratelimit: keys rate limits by c.ClientIP() — without proxy, all -// clients behind the same proxy share one rate-limit bucket +// [Config.TrustedHeaders] selects which headers to inspect (default +// "x-forwarded-for" and "x-real-ip"); X-Forwarded-For is walked right-to-left +// to defeat left-side spoofing, while any other listed header is treated as a +// single-value IP. The Disable* fields use the zero-value-enabled convention: +// [Config.DisableForwardedProto] and [Config.DisableForwardedHost] both default +// to false, so a minimal Config processes both headers. Use [Config.SkipPaths] +// or [Config.Skip] to bypass the middleware. RFC 7239 (Forwarded) is not +// implemented. // -// # Skip +// # Documentation // -// Use [Config].SkipPaths for exact path matches or [Config].Skip for -// dynamic bypass logic. +// Full guides and examples: https://goceleris.dev/docs/middleware-security package proxy diff --git a/middleware/ratelimit/doc.go b/middleware/ratelimit/doc.go index 6e51753f..46e91cd1 100644 --- a/middleware/ratelimit/doc.go +++ b/middleware/ratelimit/doc.go @@ -1,78 +1,36 @@ -// Package ratelimit provides token-bucket rate limiting middleware for -// celeris. -// -// The limiter uses a sharded token-bucket algorithm keyed by client IP -// (by default). Each key gets an independent bucket that refills at the -// configured rate up to the burst capacity. A background goroutine -// periodically evicts expired buckets. -// -// Basic usage with defaults (10 RPS, burst 20): -// -// server.Use(ratelimit.New()) -// -// Human-readable rate string (100 per minute, burst 200): -// -// server.Use(ratelimit.New(ratelimit.Config{ -// Rate: "100-M", -// Burst: 200, -// })) -// -// Custom limits keyed by API key: -// -// server.Use(ratelimit.New(ratelimit.Config{ -// RPS: 100, -// Burst: 200, -// KeyFunc: func(c *celeris.Context) string { -// return c.Header("x-api-key") -// }, -// })) -// -// The middleware sets X-RateLimit-Limit, X-RateLimit-Remaining, and -// X-RateLimit-Reset headers on every allowed response, and Retry-After -// on denied responses (429 Too Many Requests). Set DisableHeaders to -// suppress all rate limit headers. -// -// [Config].SlidingWindow enables a sliding window counter algorithm for -// smoother rate limiting near window boundaries. -// -// [Config].RateFunc allows per-request rate selection, with distinct -// limiters cached up to [Config].MaxDynamicLimiters (default 1024). -// -// [ParseRate] parses rate strings in "-" format (S, M, H, D) -// into RPS and burst values, returning an error for malformed input. -// -// [ErrTooManyRequests] is the exported sentinel error (429) returned on -// deny, usable with errors.Is for error handling in upstream middleware. -// -// Set [Config].Skip to bypass the middleware dynamically, or -// [Config].SkipPaths for exact-match path exclusions. -// -// [Config].Store allows plugging in an external storage backend (e.g., Redis) -// that implements the [Store] interface. The store handles its own rate logic. -// -// [Config].SkipFailedRequests and [Config].SkipSuccessfulRequests refund -// tokens for requests that fail (status >= 400) or succeed (status < 400), -// respectively. Store backends must implement [StoreUndo] for refunds. -// -// [Config].CleanupContext controls the lifetime of background goroutines. -// When the context is cancelled, cleanup goroutines stop. If nil, they run -// until the process exits. -// -// Note: [New] spawns one or more background goroutines for bucket eviction. -// Set CleanupContext to a cancellable context to ensure they are stopped -// when the middleware is no longer needed. -// -// # Reverse Proxy Integration -// -// The default KeyFunc uses c.ClientIP(). When behind a reverse proxy, -// install the proxy middleware via Server.Pre() so rate limits apply to -// real client IPs, not the proxy's IP: -// -// server.Pre(proxy.New(proxy.Config{ -// TrustedProxies: []string{"10.0.0.0/8"}, -// })) -// server.Use(ratelimit.New()) // rate-limits by real client IP -// -// Without proxy middleware, all clients behind the same proxy share a -// single rate-limit bucket. +// Package ratelimit provides token-bucket and sliding-window rate limiting +// middleware for Celeris. +// +// [New] returns a [celeris.HandlerFunc] that enforces per-key request limits +// using a sharded token-bucket algorithm by default. Keys default to the client +// IP via [celeris.Context.ClientIP]; supply [Config.KeyFunc] to key on anything +// else (API key, user ID, etc.). +// +// Core configuration fields: [Config.RPS] and [Config.Burst] set the static +// rate; [Config.Rate] accepts human-readable strings ("100-M", "1000-H") parsed +// by [ParseRate]. [Config.RateFunc] selects a per-request rate string, with +// distinct limiters cached up to [Config.MaxDynamicLimiters] (default 1024). +// [Config.SlidingWindow] switches to a sliding-window counter for smoother +// limiting near window boundaries. [Config.Store] plugs in an external backend +// (e.g. Redis) that implements [Store]; [StoreUndo] is the optional extension +// for token refunds used by [Config.SkipFailedRequests] and +// [Config.SkipSuccessfulRequests]. +// +// On every allowed response the middleware sets X-RateLimit-Limit, +// X-RateLimit-Remaining, and X-RateLimit-Reset headers; denied responses +// (429) also receive Retry-After. Set [Config.DisableHeaders] to suppress all +// rate-limit headers. [Config.ErrorHandler] customises the 429 response (it +// supersedes the deprecated [Config.LimitReached]). The sentinel error +// [ErrTooManyRequests] is passed to ErrorHandler and is usable with errors.Is. +// [ErrDynamicLimitersExhausted] is returned when MaxDynamicLimiters is full. +// +// [ValidateConfig] checks a [Config] for errors without panicking, useful when +// loading configuration from files or untrusted sources before calling [New]. +// [New] spawns background goroutines for bucket eviction; set +// [Config.CleanupContext] to a cancellable context to stop them when the +// middleware is no longer needed. +// +// # Documentation +// +// Full guides and examples: https://goceleris.dev/docs/middleware-traffic package ratelimit diff --git a/middleware/recovery/doc.go b/middleware/recovery/doc.go index 1ddf65dd..c3282efa 100644 --- a/middleware/recovery/doc.go +++ b/middleware/recovery/doc.go @@ -1,55 +1,35 @@ // Package recovery provides panic recovery middleware for celeris. // -// The middleware catches panics from downstream handlers, logs the -// stack trace via slog, and returns a configurable error response -// instead of crashing the server. +// The middleware catches panics from downstream handlers, logs the stack trace +// via slog, and returns a configurable error response instead of crashing the +// server. Register it as early as possible in the middleware chain so it wraps +// all other handlers. // // Basic usage with defaults (4 KB stack trace, JSON 500 response): // // server.Use(recovery.New()) // -// Custom error handler and stack size: -// -// server.Use(recovery.New(recovery.Config{ -// StackSize: 8192, -// ErrorHandler: func(c *celeris.Context, err any) error { -// return c.String(500, "Something went wrong") -// }, -// })) -// -// Set [Config].StackSize to 0 to disable stack trace capture (the panic -// value, method, and path are still logged). Set [Config].DisableLogStack -// to true to suppress panic logging entirely. -// -// # Special Panic Types -// -// Panics with [http.ErrAbortHandler] are re-panicked rather than -// recovered, preserving the standard library's abort semantics. -// -// Broken pipe and ECONNRESET errors are detected automatically and -// logged at WARN level without a stack trace. Use -// [Config].BrokenPipeHandler to customize the response for these cases. -// -// # Nested Recovery -// -// If [Config].ErrorHandler itself panics, the middleware falls back to -// a default 500 Internal Server Error response. -// -// # Log Level -// -// Set [Config].LogLevel to control the slog level for normal panic log -// entries (default: [slog.LevelError]). Broken pipe panics are always -// logged at [slog.LevelWarn] regardless of this setting. -// -// # Sentinel Errors -// -// The package exports sentinel errors for use with errors.Is: -// - [ErrPanic]: generic panic recovery -// - [ErrBrokenPipe]: broken pipe or ECONNRESET -// - [ErrPanicContextCancelled]: panic after the request context was cancelled -// - [ErrPanicResponseCommitted]: panic after the response has been committed -// -// # Middleware Order -// -// Register after logger/metrics so they see the 500 status from recovered panics. +// [New] accepts an optional [Config] to customise behaviour. Key fields: +// - [Config].ErrorHandlerErr — preferred error handler (receives a typed error). +// - [Config].ErrorHandler — legacy handler (receives any); kept for compatibility. +// - [Config].BrokenPipeHandler — custom handler for broken pipe / ECONNRESET panics. +// - [Config].StackSize — max bytes for stack trace capture (default 4096; 0 disables). +// - [Config].DisableLogStack — suppress all panic log output when true. +// - [Config].LogLevel — slog level for normal panic entries (default [slog.LevelError]). +// - [Config].Logger — slog logger (default [slog.Default]). +// - [Config].Skip / [Config].SkipPaths — skip recovery for selected requests. +// +// Panics with [http.ErrAbortHandler] are re-panicked to preserve standard +// library abort semantics. Broken pipe and ECONNRESET panics are logged at +// WARN level without a stack trace. +// +// The package exports sentinel errors for use with [errors.Is]: +// - [ErrPanic]: generic panic recovery. +// - [ErrBrokenPipe]: broken pipe or ECONNRESET. +// - [ErrPanicContextCancelled]: panic after request context was cancelled. +// - [ErrPanicResponseCommitted]: panic after response has been committed. +// +// # Documentation +// +// Full guides and examples: https://goceleris.dev/docs/error-handling package recovery diff --git a/middleware/redirect/doc.go b/middleware/redirect/doc.go index 8000a523..55314db0 100644 --- a/middleware/redirect/doc.go +++ b/middleware/redirect/doc.go @@ -1,113 +1,25 @@ // Package redirect provides HTTP redirect middleware for common URL -// normalization patterns in celeris. +// normalization patterns in Celeris. // -// Nine constructor functions cover redirect and rewrite scenarios: +// Nine constructor functions return a [celeris.HandlerFunc] and accept an +// optional [Config] to override the redirect status code or skip requests: // -// # Redirect Functions +// - [HTTPSRedirect] — redirects HTTP to HTTPS +// - [WWWRedirect] — redirects non-www to www +// - [NonWWWRedirect] — redirects www to non-www +// - [TrailingSlashRedirect] — adds a trailing slash when missing +// - [RemoveTrailingSlashRedirect] — strips a trailing slash +// - [HTTPSWWWRedirect] — redirects to HTTPS + www in one hop +// - [HTTPSNonWWWRedirect] — redirects to HTTPS + non-www in one hop +// - [TrailingSlashRewrite] — adds a trailing slash in-place (no redirect) +// - [RemoveTrailingSlashRewrite] — strips a trailing slash in-place (no redirect) // -// - [HTTPSRedirect] — redirects HTTP traffic to HTTPS -// - [WWWRedirect] — redirects non-www to www subdomain -// - [NonWWWRedirect] — redirects www to non-www host -// - [TrailingSlashRedirect] — adds trailing slash when missing -// - [RemoveTrailingSlashRedirect] — strips trailing slash +// All constructors default to HTTP 301. Set [Config].Code to 308 to +// preserve the original request method across the redirect. Query strings +// are always preserved. Register with [celeris.Server.Pre] so normalization +// runs before route lookup. // -// # Combined Redirect Functions +// # Documentation // -// - [HTTPSWWWRedirect] — redirects to HTTPS + www in a single redirect -// - [HTTPSNonWWWRedirect] — redirects to HTTPS + non-www in a single redirect -// -// # Rewrite Functions -// -// - [TrailingSlashRewrite] — adds trailing slash in-place (no redirect) -// - [RemoveTrailingSlashRewrite] — strips trailing slash in-place (no redirect) -// -// Each constructor accepts an optional [Config] to override the redirect -// status code, skip specific paths, or provide a dynamic skip function. -// The rewrite functions accept [Config] for skip logic but ignore the Code -// field since they do not send a redirect response. -// -// # Redirect Code (301 vs 308) -// -// The default redirect code is 301 (Moved Permanently). This is appropriate -// for GET requests but can cause problems for POST/PUT/DELETE: browsers and -// HTTP clients are allowed to change the request method to GET when following -// a 301 redirect (RFC 7231 §6.4.2). Set [Config].Code to 308 (Permanent -// Redirect) to guarantee the client preserves the original request method -// (RFC 7538 §3). Use 307 for the temporary equivalent that also preserves -// the method. -// -// # Query String Preservation -// -// All redirect functions preserve the original query string. The query is -// appended to the redirect URL when non-empty. -// -// # Short-Circuit Behavior -// -// When a redirect occurs, the middleware returns immediately via -// [celeris.Context.Redirect] without calling c.Next(). Downstream -// handlers are not executed. -// -// # Empty Host -// -// If [celeris.Context.Host] returns an empty string (malformed request), -// all redirect and rewrite functions pass through to c.Next() without -// modification. This prevents generating malformed redirect URLs. -// -// # Root Path Handling -// -// The trailing slash variants treat the root path "/" as a no-op. "/" is -// never modified by [TrailingSlashRedirect] (already has a slash) or -// [RemoveTrailingSlashRedirect] (root must keep its slash). -// -// # Pre-Routing Middleware -// -// This middleware is designed to run via [celeris.Server.Pre] so that URL -// normalization occurs before route lookup: -// -// server.Pre(redirect.HTTPSRedirect()) -// server.Pre(redirect.TrailingSlashRedirect()) -// -// # Redirect Loops -// -// Be careful not to combine conflicting redirect middleware. For example, -// using both [TrailingSlashRedirect] and [RemoveTrailingSlashRedirect] -// creates an infinite redirect loop. Similarly, using both [WWWRedirect] -// and [NonWWWRedirect] causes a loop. The combined constructors -// [HTTPSWWWRedirect] and [HTTPSNonWWWRedirect] avoid the double-redirect -// that would occur from chaining [HTTPSRedirect] with [WWWRedirect] or -// [NonWWWRedirect]. -// -// # Reverse Proxy Interaction -// -// When running behind a TLS-terminating reverse proxy, install the proxy -// middleware via Server.Pre() BEFORE redirect middleware. Without it, -// Scheme() returns "http" (the backend transport), causing an infinite -// redirect loop with HTTPSRedirect. -// -// # Choosing a Constructor -// -// The package provides 9 constructors organized into three categories: -// -// Redirect (sends 3xx response): -// -// HTTPSRedirect, WWWRedirect, NonWWWRedirect, -// TrailingSlashRedirect, RemoveTrailingSlashRedirect -// -// Combined redirect (avoids double-redirect): -// -// HTTPSWWWRedirect, HTTPSNonWWWRedirect -// -// Rewrite (modifies path in-place, no redirect): -// -// TrailingSlashRewrite, RemoveTrailingSlashRewrite -// -// Use redirect when clients should update their URLs (SEO, bookmarks). -// Use rewrite when normalization is internal (the client URL stays the same). -// Use combined constructors when both scheme and host need changing. -// -// # Skipping -// -// Use [Config].Skip for dynamic skip logic or [Config].SkipPaths for -// exact-match path exclusions. Skipped requests call c.Next() without -// any redirect. +// Full guides and examples: https://goceleris.dev/docs/middleware-routing-helpers package redirect diff --git a/middleware/requestid/doc.go b/middleware/requestid/doc.go index f81fe435..cf32b8b5 100644 --- a/middleware/requestid/doc.go +++ b/middleware/requestid/doc.go @@ -1,64 +1,22 @@ // Package requestid provides request ID middleware for celeris. // -// The middleware reads the request ID from the incoming X-Request-Id -// header (configurable). If the incoming ID is absent or invalid, a -// new UUID v4 is generated using a buffered random source that batches -// 256 UUIDs per crypto/rand syscall. The ID is set on both the response -// header and the context store under [ContextKey] ("request_id"). +// [New] reads the request ID from the incoming header (default: "x-request-id"). +// If absent or invalid (non-printable ASCII, >128 bytes), a UUID v4 is generated +// using a buffered random source that batches 256 UUIDs per crypto/rand syscall. +// The ID is written to the response header and stored in the context under +// [ContextKey] ("request_id"). // -// Basic usage: +// Key exported symbols: +// - [New] — constructor; accepts an optional [Config]. +// - [Config] — Header, Generator, DisableTrustProxy, EnableStdContext, Skip, SkipPaths. +// - [FromContext] — retrieve the ID from a [celeris.Context] in downstream handlers. +// - [FromStdContext] — retrieve the ID from a stdlib [context.Context] (requires Config.EnableStdContext). +// - [CounterGenerator] — monotonic "{prefix}-{N}" generator; zero syscalls after init. +// - [ContextKey] — the context-store key ("request_id"). // -// server.Use(requestid.New()) +// Register before logger so every log line carries the request ID. // -// Custom header and deterministic generator for testing: +// # Documentation // -// server.Use(requestid.New(requestid.Config{ -// Header: "x-trace-id", -// Generator: requestid.CounterGenerator("req"), -// })) -// -// # Input Validation -// -// Incoming IDs are validated: only printable ASCII (0x20-0x7E) is -// accepted, with a maximum length of 128 characters. Invalid or -// oversized IDs are silently replaced with a fresh UUID. -// -// # DisableTrustProxy -// -// [Config].DisableTrustProxy controls whether the inbound request header -// is accepted. When false (default), a valid inbound header is propagated -// as-is. When true, the inbound header is always ignored and a fresh ID -// is generated: -// -// server.Use(requestid.New(requestid.Config{ -// DisableTrustProxy: true, -// })) -// -// # Retrieving the Request ID -// -// Use [FromContext] to retrieve the request ID from downstream handlers: -// -// id := requestid.FromContext(c) -// -// Use [FromStdContext] to retrieve it from a stdlib [context.Context]: -// -// id := requestid.FromStdContext(ctx) -// -// # Skipping -// -// Use [Config].Skip for dynamic skip logic or [Config].SkipPaths for -// path exclusions. SkipPaths uses exact path matching. -// -// # Custom Generator Validation -// -// Custom generator output is validated with the same rules as inbound -// headers. If the generator returns an invalid or empty string, it is -// retried up to 3 times before falling back to the built-in UUID. -// -// [CounterGenerator] returns a monotonic ID generator ("{prefix}-{N}") -// with zero syscalls after initialization. -// -// # Middleware Order -// -// Register first -- all downstream middleware can access the request ID. +// Full guides and examples: https://goceleris.dev/docs/observability#request-ids package requestid diff --git a/middleware/rewrite/doc.go b/middleware/rewrite/doc.go index 25054c23..0a100fa1 100644 --- a/middleware/rewrite/doc.go +++ b/middleware/rewrite/doc.go @@ -1,122 +1,35 @@ -// Package rewrite provides URL rewrite middleware for celeris using -// regular expression pattern matching. -// -// The middleware matches the request path against a set of regex rules -// and either rewrites the path in-place (silent rewrite) or sends an -// HTTP redirect response. -// -// Basic usage (silent rewrite -- use anchored patterns): -// -// server.Pre(rewrite.New(rewrite.Config{ -// Rules: []rewrite.Rule{ -// {Pattern: "^/old$", Replacement: "/new"}, -// }, -// })) -// -// Redirect mode: -// -// server.Pre(rewrite.New(rewrite.Config{ -// Rules: []rewrite.Rule{ -// {Pattern: "^/old$", Replacement: "/new"}, -// }, -// RedirectCode: 301, -// })) -// -// # Regex and Capture Groups -// -// Rule patterns are Go regular expressions compiled with [regexp.MustCompile]. -// The replacement string supports capture group substitution ($1, $2, ...) -// using [regexp.Regexp.ReplaceAllString] semantics: -// -// server.Pre(rewrite.New(rewrite.Config{ -// Rules: []rewrite.Rule{ -// {Pattern: `/users/(\d+)/posts`, Replacement: "/api/v2/users/$1/posts"}, -// }, -// })) -// -// # First-Match-Wins -// -// Rules are evaluated in the order provided. The first matching regex wins -// and subsequent rules are not checked: -// -// Rules: []rewrite.Rule{ -// {Pattern: "/a/.*", Replacement: "/alpha"}, // checked first -// {Pattern: "/b/.*", Replacement: "/beta"}, // checked second -// } -// -// # Silent Rewrite vs Redirect -// -// When RedirectCode is 0 (default), the middleware calls [celeris.Context.SetPath] -// to modify the request path in-place. The client URL remains unchanged and -// downstream handlers see the rewritten path. -// -// When RedirectCode is a valid redirect status (301, 302, 303, 307, 308), -// the middleware sends an HTTP redirect response. The redirect URL preserves -// the original scheme, host, and query string. Downstream handlers are not -// executed. -// -// # Pre-Routing Middleware -// -// This middleware is designed to run via [celeris.Server.Pre] so that URL -// rewriting occurs before route lookup: +// Package rewrite provides pre-routing URL rewrite middleware for celeris, +// matching the request path against ordered regular-expression rules. +// +// Construct the middleware with [New] and a [Config], then register it via +// [celeris.Server.Pre] so rewriting happens before route lookup. Each [Rule] +// has a Pattern (a Go regexp) and a Replacement that supports capture-group +// substitution ($1, $2, ...). Rules are evaluated in order and the first +// match wins. +// +// A rule rewrites silently or redirects depending on RedirectCode. When zero +// (the default), the path is rewritten in place via [celeris.Context.SetPath] +// and downstream handlers see the new path. When set to a redirect status +// (301, 302, 303, 307, 308), the middleware sends an HTTP redirect preserving +// the scheme, host, and query string. RedirectCode may be set on [Config] as +// the default or overridden per [Rule]. A rule can be scoped with Methods and +// Host; both default to matching everything. Use [Config].Skip or +// [Config].SkipPaths to bypass requests. +// +// [New] compiles patterns once via [regexp.MustCompile] and panics on an empty +// Rules slice, an invalid pattern, or an invalid non-zero RedirectCode. // // server.Pre(rewrite.New(rewrite.Config{ // Rules: []rewrite.Rule{ -// {Pattern: "/old", Replacement: "/new"}, +// {Pattern: `^/api/v1/(.*)$`, Replacement: "/api/v2/$1"}, // }, // })) // -// # Query String Preservation -// -// In redirect mode, the original query string is appended to the redirect -// URL. In silent rewrite mode, the query string is unmodified since only -// the path is rewritten. -// -// # Conditional Rewriting -// -// Rules can be restricted to specific HTTP methods or hosts: -// -// server.Pre(rewrite.New(rewrite.Config{ -// Rules: []rewrite.Rule{ -// { -// Pattern: "^/api/v1/(.*)$", -// Replacement: "/api/v2/$1", -// Methods: []string{"GET", "HEAD"}, -// }, -// { -// Pattern: "^/admin/(.*)$", -// Replacement: "/internal/$1", -// Host: "admin.example.com", -// }, -// }, -// })) -// -// When Methods is empty, the rule matches all methods. When Host is -// empty, the rule matches all hosts. -// -// # Init-Time Validation -// -// [New] panics if Rules is empty, if RedirectCode is non-zero and not a -// valid redirect status, or if any rule pattern is an invalid regex (via -// [regexp.MustCompile]). -// -// # Security -// -// Regex patterns are compiled once at init time via [regexp.MustCompile], -// not per-request. Avoid catastrophic backtracking patterns (e.g., -// `(a+)+$`) which can cause high CPU during compilation. -// -// In redirect mode, the redirect URL is constructed from [celeris.Context.Host] -// and [celeris.Context.Scheme], which are derived from request headers. -// Without a reverse proxy that validates the Host header, an attacker -// can send a crafted Host to produce an open redirect. Ensure your -// deployment validates the Host header upstream (e.g., via the proxy -// middleware) or use silent rewrite mode (RedirectCode: 0) which only -// modifies the internal path. +// In redirect mode the redirect URL is built from the request's Host and +// Scheme headers; validate the Host header upstream (or use silent rewrite) +// to avoid open redirects. // -// # Skipping +// # Documentation // -// Use [Config].Skip for dynamic skip logic or [Config].SkipPaths for -// exact-match path exclusions. Skipped requests call c.Next() without -// any rewrite. +// Full guides and examples: https://goceleris.dev/docs/middleware-routing-helpers package rewrite diff --git a/middleware/secure/doc.go b/middleware/secure/doc.go index 7c7025a4..6df1c704 100644 --- a/middleware/secure/doc.go +++ b/middleware/secure/doc.go @@ -1,9 +1,8 @@ // Package secure provides OWASP security headers middleware for celeris. // -// The middleware sets a comprehensive suite of HTTP security headers on -// every response. All non-empty header values are pre-computed into a flat -// slice at initialization. The hot path iterates this slice with zero -// allocations. +// The middleware sets a comprehensive suite of HTTP security headers on every +// response. All non-empty header values are pre-computed into a flat slice at +// initialization; the hot path iterates this slice with zero allocations. // // Default headers set on every response: // @@ -18,56 +17,16 @@ // - X-Permitted-Cross-Domain-Policies: "none" // - Origin-Agent-Cluster: "?1" // -// Cross-Origin-Embedder-Policy ("require-corp") and X-Download-Options -// ("noopen") are OPT-IN (off by default, #338): COEP-by-default breaks -// cross-origin resource loads (matching helmet, which leaves it off), and -// X-Download-Options only affected legacy Internet Explorer. Content-Security- -// Policy and Permissions-Policy are likewise only emitted when their [Config] -// fields are non-empty. +// Cross-Origin-Embedder-Policy and X-Download-Options are opt-in (off by +// default). Content-Security-Policy and Permissions-Policy are only emitted +// when their [Config] fields are non-empty. // -// # Usage +// Use [New] to create the middleware. Pass a [Config] to override defaults. +// Set any string field to [Suppress] ("-") to omit that individual header. +// Use [Config].DisableHSTS to omit Strict-Transport-Security entirely. +// Use [Config].Skip or [Config].SkipPaths to bypass the middleware per request. // -// Default configuration (all OWASP-recommended headers): +// # Documentation // -// server.Use(secure.New()) -// -// Custom configuration: -// -// server.Use(secure.New(secure.Config{ -// ContentSecurityPolicy: "default-src 'self'", -// HSTSMaxAge: 31536000, -// HSTSPreload: true, -// })) -// -// # Suppressing Individual Headers -// -// Set any string field to [Suppress] ("-") to omit that header: -// -// server.Use(secure.New(secure.Config{ -// XFrameOptions: secure.Suppress, -// })) -// -// For HSTS, set [Config].DisableHSTS to true to omit the header entirely. -// HSTSMaxAge defaults to 2 years (63072000 seconds) whether or not other -// fields are customized — there is no zero-value trap. -// -// # HSTS Preload Validation -// -// When [Config].HSTSPreload is true, validate() panics at initialization -// if HSTSMaxAge < 31536000 or HSTSExcludeSubdomains is true, enforcing -// HSTS preload list requirements. -// -// # Skipping -// -// Use [Config].Skip for dynamic skip logic or [Config].SkipPaths for -// exact-match path exclusions. Skipped requests receive no security -// headers at all. -// -// # CORS Interaction -// -// When using CORS middleware alongside secure, note that the default -// CrossOriginResourcePolicy (same-origin) blocks cross-origin resource -// loading; APIs serving cross-origin requests should set -// CrossOriginResourcePolicy: "cross-origin". COEP is off by default so it no -// longer needs suppressing; enable it explicitly only when isolating the page. +// Full guides and examples: https://goceleris.dev/docs/middleware-security package secure diff --git a/middleware/session/doc.go b/middleware/session/doc.go index 0796a5b8..ba3e735e 100644 --- a/middleware/session/doc.go +++ b/middleware/session/doc.go @@ -2,78 +2,34 @@ // celeris. // // Sessions are identified by a cookie (default), header, or query parameter, -// and data is stored server-side in a pluggable [Store]. The default -// [MemoryStore] uses sharded maps with a background cleanup goroutine, +// and data is stored server-side in a pluggable [Store]. The built-in +// [NewMemoryStore] uses sharded maps with a background cleanup goroutine, // suitable for single-instance deployments. // -// Basic usage with defaults (in-memory store, 24h sessions): +// Attach the middleware with defaults (in-memory store, 24 h sessions): // // server.Use(session.New()) // -// Custom cookie name and timeouts: -// -// server.Use(session.New(session.Config{ -// CookieName: "myapp_sid", -// IdleTimeout: 15 * time.Minute, -// AbsoluteTimeout: 2 * time.Hour, -// })) -// -// # Accessing the Session -// -// Use [FromContext] to retrieve the session from downstream handlers: +// Retrieve and mutate the session in downstream handlers via [FromContext]: // // s := session.FromContext(c) // s.Set("user", "admin") // name, ok := s.Get("user") // -// Modified sessions are automatically saved after the handler chain returns. -// Call [Session.Destroy] to invalidate a session, or [Session.Regenerate] to -// issue a new session ID. -// -// # Session Fixation Prevention -// -// Applications MUST call [Session.Regenerate] (or [Session.Reset]) after -// any authentication state change to prevent session fixation attacks: -// -// func loginHandler(c *celeris.Context) error { -// // ... validate credentials ... -// s := session.FromContext(c) -// if err := s.Regenerate(); err != nil { -// return err -// } -// s.Set("user", authenticatedUser) -// return nil -// } -// -// # Pluggable Extractors +// Modified sessions are saved automatically after the handler chain returns. +// Call [Session.Destroy] to invalidate a session, [Session.Regenerate] to +// issue a new session ID (required after any authentication state change to +// prevent session fixation), or [Session.SetIdleTimeout] to override the +// per-session idle window ("remember me" flows). // // [CookieExtractor], [HeaderExtractor], [QueryExtractor], and -// [ChainExtractor] control where the session ID is read from. When a -// non-cookie extractor is used, the session ID is returned in a response -// header named after [Config].CookieName so API clients can capture it. -// -// # Out-of-Band Access -// -// [NewHandler] returns a [Handler] providing both the middleware and -// out-of-band session access via [Handler.GetByID] for admin tools or -// background jobs. -// -// # Timeout Semantics -// -// IdleTimeout (default 30m) controls server-side expiry per session. -// AbsoluteTimeout (default 24h) caps total session lifetime. Set -// AbsoluteTimeout to -1 to disable it. [Session.SetIdleTimeout] -// overrides idle timeout for individual sessions ("remember me" flows). -// -// # Custom Stores -// -// Implement the [Store] interface to back sessions with any storage -// backend. All [Store] methods receive a [context.Context] propagated -// from the request for cancellation and deadline support. +// [ChainExtractor] control where the session ID is read from. For +// out-of-band access (admin tools, background jobs) use [NewHandler], which +// exposes the middleware via [Handler.Middleware] and direct lookup via +// [Handler.GetByID]. Implement the [Store] interface to back sessions with +// any storage backend. // -// # Security +// # Documentation // -// CookieSecure defaults to false for development convenience. Production -// deployments MUST set CookieSecure: true to prevent cookie transmission -// over unencrypted connections. +// Full guides and examples: https://goceleris.dev/docs/data-stores package session diff --git a/middleware/singleflight/doc.go b/middleware/singleflight/doc.go index 085deaeb..2e815e79 100644 --- a/middleware/singleflight/doc.go +++ b/middleware/singleflight/doc.go @@ -1,139 +1,26 @@ -// Package singleflight provides request deduplication middleware for celeris. +// Package singleflight provides request-coalescing middleware for celeris. // -// When multiple identical requests arrive concurrently, only the first -// (the "leader") executes the handler chain. Subsequent requests (the -// "waiters") block until the leader completes, then receive a copy of -// the leader's response. This prevents the thundering herd problem where -// a popular endpoint receives a burst of identical requests that all hit -// the backend simultaneously. +// When several identical requests arrive concurrently, only the first (the +// "leader") executes the handler chain; the rest (the "waiters") block until +// the leader finishes and then receive a copy of its response. This absorbs +// thundering-herd bursts on hot endpoints so they hit the backend once. // -// Basic usage: +// Install it with [New], optionally passing a [Config]. The zero-value +// configuration deduplicates on method + path + sorted query string + +// Authorization + Cookie, so requests from different authenticated users are +// never coalesced. Use [Config.KeyFunc] to change the key, and [Config.Skip] +// or [Config.SkipPaths] to exclude requests (for example non-idempotent +// methods or large-response endpoints). Waiter responses carry an +// "x-singleflight: HIT" header. // // server.Use(singleflight.New()) // -// Custom key function: +// Singleflight buffers the leader's response, so install it after timeout +// middleware and before response transforms such as compress or etag. It is +// intended for idempotent reads; a custom KeyFunc that returns user-specific +// data must incorporate user identity to avoid cross-user leakage. // -// server.Use(singleflight.New(singleflight.Config{ -// KeyFunc: func(c *celeris.Context) string { -// return c.Path() // ignore query parameters -// }, -// })) +// # Documentation // -// # Algorithm -// -// The middleware maintains an in-memory map of in-flight keys. For each -// incoming request: -// -// 1. Compute the deduplication key via [Config].KeyFunc. -// 2. Lock the group and check the map. -// 3. If the key exists, the request is a waiter: unlock, wait for the -// leader to finish, then replay the captured response. -// 4. If the key is absent, the request is the leader: register the key, -// unlock, buffer the response, execute c.Next(), capture the result, -// remove the key from the map, and wake all waiters. -// -// The embedded singleflight group uses no external dependencies. -// -// # Default Key -// -// The default key function produces: method + "\x00" + path + "\x00" + -// sorted query string + "\x00" + Authorization header + "\x00" + Cookie -// header. Query parameters are sorted via [url.Values.Encode] so that -// ?a=1&b=2 and ?b=2&a=1 produce the same key. Multi-value query -// parameters are also sorted within each key so that ?a=2&a=1 and -// ?a=1&a=2 produce the same key. When the request has no query string, -// the query component is omitted entirely (no parsing overhead). -// Authorization and Cookie components are omitted when absent. -// -// # x-singleflight Response Header -// -// Waiter responses include the header "x-singleflight: HIT" so that -// callers (and observability tools) can distinguish coalesced responses -// from leader responses. The leader response does not carry this header. -// -// # Middleware Ordering -// -// Singleflight should run AFTER timeout middleware (so each coalesced -// request respects its own timeout) and BEFORE transform middleware -// like compress or etag (so the response is captured before transformation): -// -// server.Use(timeout.New(...)) // outermost -// server.Use(singleflight.New()) // dedup on uncompressed response -// server.Use(compress.New()) // innermost -// server.Use(etag.New()) -// -// # Idempotency -// -// Singleflight is designed for idempotent read endpoints (GET, HEAD). -// Using it on non-idempotent methods (POST, PUT, DELETE) may cause -// unintended behavior: only one request executes and all waiters receive -// the same response. For most applications, skip non-idempotent methods: -// -// singleflight.New(singleflight.Config{ -// Skip: func(c *celeris.Context) bool { -// m := c.Method() -// return m != "GET" && m != "HEAD" -// }, -// }) -// -// If your endpoint modifies state, either skip it with [Config].SkipPaths -// or use a [Config].KeyFunc that differentiates by request body or session. -// -// # Error Propagation -// -// If the leader's handler returns an error, all waiters receive the same -// error. The error is propagated as-is (including [celeris.HTTPError] -// with its status code). -// -// # Panic Propagation -// -// If the leader's handler panics, the panic value is captured and -// re-panicked in every waiter goroutine (and in the leader after -// cleanup). This ensures recovery middleware further up the chain -// catches the panic in every request context. -// -// # Non-2xx Responses -// -// Non-2xx responses (404, 500, etc.) are coalesced just like 2xx -// responses. The middleware does not distinguish between success and -// failure status codes — it deduplicates all in-flight requests for -// the same key. -// -// # Security: Authenticated Endpoints -// -// The default key function includes Authorization and Cookie request -// headers, so requests from different authenticated users produce different -// keys and are NOT coalesced. This prevents cross-user data leakage. -// -// If you provide a custom KeyFunc, ensure it incorporates user identity -// for any endpoint that returns user-specific data. Failing to do so -// will cause one user's response (including Set-Cookie headers and -// personalized content) to be replayed to other concurrent users. -// -// # Waiter Timeout -// -// Waiters block unconditionally until the leader completes. There is no -// independent timeout per waiter — a waiter whose context deadline expires -// will still wait for the leader to finish. To bound waiter wait time, -// place timeout middleware OUTSIDE singleflight (the recommended ordering): -// -// server.Use(timeout.New(...)) // bounds total request time -// server.Use(singleflight.New()) // waiter wait is bounded by timeout -// -// This is the same limitation as [golang.org/x/sync/singleflight]. -// -// # Memory -// -// The leader's response body is deep-copied for each waiter. For large -// responses with many concurrent waiters, this multiplies memory usage. -// Use Skip or SkipPaths to exclude large-response endpoints. -// -// # Skipping -// -// Use [Config].Skip for dynamic skip logic or [Config].SkipPaths for -// path exclusions. SkipPaths uses exact path matching: -// -// server.Use(singleflight.New(singleflight.Config{ -// SkipPaths: []string{"/admin", "/webhook"}, -// })) +// Full guides and examples: https://goceleris.dev/docs/middleware-traffic package singleflight diff --git a/middleware/sse/doc.go b/middleware/sse/doc.go index a14c44cd..7c576571 100644 --- a/middleware/sse/doc.go +++ b/middleware/sse/doc.go @@ -1,89 +1,30 @@ // Package sse provides Server-Sent Events (SSE) middleware for celeris. // -// SSE enables unidirectional server-to-client streaming over HTTP. This -// middleware manages event formatting, heartbeat keep-alives, reconnection -// via Last-Event-ID, and client disconnect detection. +// SSE enables unidirectional server-to-client streaming over HTTP. Use [New] +// with a [Config] to attach an SSE handler to any route. The [Config.Handler] +// field receives a [*Client] per connection; call [Client.Send] or +// [Client.SendData] to push [Event] values, and watch [Client.Context] for +// disconnect. // -// # Basic Usage +// For fan-out to many subscribers, use [NewBroker] ([BrokerConfig]): call +// [Broker.Subscribe] in each handler and [Broker.Publish] from any goroutine. +// [Broker.Publish] formats each event once via [PreparedEvent] and never +// blocks on a slow subscriber. // -// server.GET("/events", sse.New(sse.Config{ -// Handler: func(client *sse.Client) { -// ticker := time.NewTicker(time.Second) -// defer ticker.Stop() -// for { -// select { -// case <-client.Context().Done(): -// return -// case t := <-ticker.C: -// if err := client.Send(sse.Event{ -// Event: "time", -// Data: t.Format(time.RFC3339), -// }); err != nil { -// return -// } -// } -// } -// }, -// })) +// Backpressure is controlled per-client via [Config.MaxQueueDepth] and +// [Config.OnSlowClient] (drop/close policy), and per-broker-subscriber via +// [BrokerConfig.SubscriberBuffer]. // -// # Reconnection +// Reconnection replay is available via [Config.ReplayStore]: supply +// [NewRingBuffer] for in-process replay or [NewKVReplayStore] for +// distributed replay backed by a KV store. The middleware calls +// [Client.LastEventID] and replays missed events automatically. // -// When a client reconnects, browsers send a Last-Event-ID header. Use -// [Client.LastEventID] to resume from where the client left off: +// Heartbeat comments are sent every [DefaultHeartbeatInterval] (15 s) by +// default; configure with [Config.HeartbeatInterval]. The middleware works +// with all celeris engines and handles stream lifecycle internally. // -// Handler: func(client *sse.Client) { -// lastID := client.LastEventID() -// events := fetchEventsSince(lastID) -// for _, e := range events { -// client.Send(e) -// } -// }, +// # Documentation // -// For automatic replay see [Config.ReplayStore] + [NewRingBuffer] / -// [NewKVReplayStore]: the middleware will Append every Send and replay -// missed events on the next reconnect with no work from the handler. -// -// # Backpressure: two layers, two purposes -// -// celeris exposes per-subscriber backpressure at TWO distinct layers: -// -// - [Config.MaxQueueDepth] / [Config.OnSlowClient] is the per-Client -// queue. When set, [Client.Send] enqueues onto a bounded channel -// that a per-Client drain goroutine writes to the wire. The user's -// [Config.OnSlowClient] policy fires when that queue overflows. -// Use this layer when a single subscriber drives the load — e.g. -// a long-lived per-user feed where Send is the only producer. -// -// - [Broker] / [BrokerConfig.SubscriberBuffer] / [OnSlowSubscriber] -// is the per-subscriber queue INSIDE the broker. [Broker.Publish] -// fans out via these queues; per-subscriber drain goroutines call -// [Client.WritePreparedEvent] (which writes synchronously to the -// wire, bypassing Client.MaxQueueDepth). Use this layer for -// fan-out from a single publisher to N subscribers — the broker -// formats each event ONCE and never blocks on a single slow -// subscriber. -// -// The two layers are orthogonal. A Broker.Subscribe'd client can also -// have a non-zero MaxQueueDepth — the broker uses WritePreparedEvent -// (which is always synchronous) so the Client.queue lies dormant -// unless something else calls [Client.Send] directly. In that case -// the Client.queue absorbs Send-bursts independently of the broker -// fan-out. There is no scenario where both queues fight each other. -// -// # Heartbeat -// -// By default, a heartbeat comment is sent every 15 seconds to detect -// disconnected clients. Configure via [Config.HeartbeatInterval] or -// disable with a negative value. -// -// # Engine Compatibility -// -// SSE works with all celeris engines (std, epoll, io_uring). The middleware -// handles [celeris.Context.Detach] internally — callers do not need to manage -// the event loop lifecycle. -// -// # CORS -// -// For cross-origin EventSource connections, configure CORS middleware on -// the SSE endpoint to allow the appropriate origin. +// Full guides and examples: https://goceleris.dev/docs/sse package sse diff --git a/middleware/static/doc.go b/middleware/static/doc.go index 90f0c760..f4ba8655 100644 --- a/middleware/static/doc.go +++ b/middleware/static/doc.go @@ -1,113 +1,23 @@ // Package static serves static files from the OS filesystem or an [fs.FS]. // -// Basic usage with an OS directory: -// -// server.Use(static.New(static.Config{Root: "./public"})) -// -// With an embedded filesystem: -// -// //go:embed assets/* -// var assets embed.FS -// server.Use(static.New(static.Config{FS: assets})) -// -// With a URL prefix: -// -// server.Use(static.New(static.Config{ -// Root: "./public", -// Prefix: "/static", -// })) -// -// # Prefix Matching -// -// The Prefix is matched at segment boundaries. A prefix of "/api" will match -// "/api/file.txt" but NOT "/api-docs/file.txt". This prevents unintended -// path collisions. -// -// # Directory Browsing -// -// When Browse is enabled and the request path resolves to a directory without -// an index file, an HTML directory listing is rendered. Filenames in the -// listing are URL-encoded in href attributes and HTML-escaped in display -// text to prevent XSS via crafted filenames. -// -// # SPA Mode -// -// When [Config].SPA is true the middleware operates in single-page -// application mode: requests for non-existent files serve the index file -// (default index.html) instead of falling through to the next handler. -// This allows client-side routers to handle arbitrary URL paths. Existing -// files and directories are still served normally. -// -// # Caching -// -// The middleware sets Last-Modified and ETag headers based on file metadata. -// Conditional requests (If-Modified-Since, If-None-Match) return 304 Not -// Modified when the client already has a fresh copy. -// -// The ETag is a weak validator computed from the file's modification time -// and size: W/"-". For embed.FS files where ModTime -// is zero, caching headers are omitted. -// -// # Cache-Control -// -// [Config].MaxAge sets the Cache-Control max-age directive. When MaxAge is -// greater than zero the response includes a "public, max-age=N" header -// (where N is seconds). When MaxAge is zero (the default) no Cache-Control -// header is added. MaxAge works alongside ETag and Last-Modified: browsers -// that have a cached copy within the max-age window skip the network -// entirely, and once the window expires they can still use conditional -// requests to validate freshness. -// -// # Range Requests -// -// OS files support range requests via the core [celeris.Context.FileFromDir] -// method. For fs.FS files, the middleware implements range request handling -// inline, supporting single byte ranges (bytes=start-end). When the underlying -// fs.File implements [io.ReadSeeker], range requests seek to the requested -// offset and read only the required bytes instead of loading the entire file -// into memory. -// -// # Content-Type Detection -// -// Content types are determined by file extension via [mime.TypeByExtension]. -// When the extension is unknown or absent, the middleware sniffs the first 512 -// bytes using [http.DetectContentType] (for fs.FS files) or delegates to the -// core file-serving method (for OS files). -// -// # Pre-Compressed Files -// -// When [Config].Compress is true, the middleware checks for pre-compressed -// variants of the requested file before serving the original. It inspects -// the Accept-Encoding request header and looks for a .br (Brotli) or .gz -// (gzip) file alongside the original. Brotli is preferred when both are -// accepted. The response includes Content-Encoding and Vary headers. -// -// Pre-compressed file serving only applies to OS filesystem (Root), not -// fs.FS. This pairs well with build tools that generate .br and .gz files -// at deploy time (e.g. Vite, esbuild, or a post-build compression step). -// -// # Security -// -// OS files are served through [celeris.Context.FileFromDir], which prevents -// directory traversal and symlink escape. For fs.FS files, paths are cleaned -// before opening. -// -// # Method Filtering -// -// Only GET and HEAD requests are processed. All other methods pass through -// to the next handler. -// -// # Ordering -// -// Place the static middleware after security and authentication middleware -// if you need to protect static files. Place it before compress and etag -// if you want framework-level caching/compression (though the static -// middleware sets its own ETag from file metadata, which the etag middleware -// will preserve). -// -// # Skipping -// -// Use [Config].Skip for dynamic skip logic or [Config].SkipPaths for -// exact-match path exclusions. Skipped requests call c.Next() without -// serving any files. +// Use [New] with a [Config] to mount the middleware. Set [Config].Root for an +// OS directory or [Config].FS for an embedded/virtual filesystem (mutually +// exclusive; FS takes precedence when both are set). Key options: +// +// - [Config].Prefix — URL path prefix, matched at segment boundaries. +// - [Config].Index — directory index filename (default "index.html"). +// - [Config].Browse — enable HTML directory listings. +// - [Config].SPA — single-page app mode: unknown paths serve the index file. +// - [Config].MaxAge — Cache-Control max-age duration (zero = no header). +// - [Config].Compress — serve pre-compressed .br/.gz variants when accepted. +// - [Config].Skip / [Config].SkipPaths — skip the middleware dynamically or +// by exact path match. +// +// Only GET and HEAD requests are processed; all others pass through. The +// middleware sets Last-Modified, ETag (weak, mtime+size), and optional +// Cache-Control headers and handles conditional requests (304 Not Modified). +// +// # Documentation +// +// Full guides and examples: https://goceleris.dev/docs/static-files package static diff --git a/middleware/store/doc.go b/middleware/store/doc.go index b7e16b0a..a4e8875a 100644 --- a/middleware/store/doc.go +++ b/middleware/store/doc.go @@ -1,42 +1,44 @@ // Package store defines the unified byte-level key-value interface shared -// by celeris middleware (session, csrf, ratelimit, jwt, cache, idempotency). +// by celeris middleware (session, csrf, cache, idempotency, SSE replay). // -// # Interfaces +// # Core interface // // [KV] is the minimal required surface: Get, Set with TTL, Delete. All // implementations must be safe for concurrent use and honor the contract -// that Get returns (nil, [ErrNotFound]) on a missing key. -// -// Optional extension interfaces ([GetAndDeleter], [Scanner], [PrefixDeleter], -// [SetNXer], [Scripter]) surface backend capabilities. Middleware feature- -// detect these via type assertion and fall back to emulation or no-op -// semantics when a backend does not implement them. -// -// # Reference backends -// -// - [MemoryKV] — in-memory, sharded implementation. Implements every -// optional extension. Used as the default store and in tests. -// - middleware/session/redisstore — Redis-backed via driver/redis. -// Implements KV + Scanner; GetAndDeleter when Redis >= 6.2. -// - middleware/session/postgresstore — PostgreSQL-backed via driver/postgres. -// Implements KV only; Reset via TRUNCATE (exposed as io.Closer). -// - middleware/csrf/redisstore — Redis-backed CSRF storage. -// Implements KV + GetAndDeleter (GETDEL). -// - middleware/ratelimit/redisstore — Redis-backed ratelimit store. -// Implements ratelimit.Store via EVALSHA; not a KV adapter. -// -// # Response wire format -// -// [EncodedResponse] + [ResponseWireVersion] define a byte-efficient -// snapshot format used by cache and idempotency middleware to persist -// captured HTTP responses. The format is versioned to allow forward -// compatibility; decoders reject unknown versions. -// -// # Contract summary -// -// - Get returns (nil, [ErrNotFound]) on miss. (nil, nil) is forbidden. -// - Set with ttl == 0 stores with no expiry. -// - Delete is idempotent: deleting a missing key returns nil. -// - Values returned by Get are caller-owned; backends must copy. -// - All methods are safe for concurrent use. +// that Get returns (nil, [ErrNotFound]) on a missing key; returning +// (nil, nil) is forbidden. +// +// Optional extension interfaces surface backend capabilities; middleware +// feature-detects them via type assertion and falls back to emulation +// or no-op semantics when a backend does not implement them: +// +// - [GetAndDeleter] — atomic GET+DEL (e.g. Redis GETDEL); used by csrf. +// - [Scanner] — key enumeration by prefix; used by session Reset. +// - [PrefixDeleter] — bulk delete by prefix; used by cache InvalidatePrefix. +// - [SetNXer] — atomic set-if-not-exists; used by idempotency lock acquisition. +// - [Counter] — atomic monotonic counter (INCR); used by SSE replay for cross-process IDs. +// - [Scripter] — server-side atomic scripts (EVALSHA); used by ratelimit Redis adapter. +// +// # Reference implementations +// +// - [MemoryKV] / [NewMemoryKV] — in-memory sharded store. Implements KV, +// GetAndDeleter, Scanner, PrefixDeleter, SetNXer, and Counter. +// Default store for single-process deployments and tests. +// - middleware/session/redisstore — Redis-backed; KV + Scanner + GetAndDeleter. +// - middleware/session/postgresstore — PostgreSQL-backed; KV only. +// - middleware/csrf/redisstore — Redis-backed CSRF; KV + GetAndDeleter. +// - middleware/ratelimit/redisstore — Redis-backed ratelimit via EVALSHA; implements Scripter. +// +// # Utilities +// +// [Prefixed] wraps any [KV] so all keys are transparently namespaced with a +// prefix — useful when sharing one backend among multiple middleware. +// [EncodeJSON] / [DecodeJSON] are convenience helpers for adapters that +// persist structured payloads through the byte-level [KV] surface. +// [EncodedResponse] + [ResponseWireVersion] define the versioned wire format +// used by cache and idempotency middleware to persist captured HTTP responses. +// +// # Documentation +// +// Full guides and examples: https://goceleris.dev/docs/data-stores package store diff --git a/middleware/swagger/doc.go b/middleware/swagger/doc.go index 7b21048f..40dbf93f 100644 --- a/middleware/swagger/doc.go +++ b/middleware/swagger/doc.go @@ -1,159 +1,34 @@ -// Package swagger provides OpenAPI specification viewer middleware for -// celeris. -// -// The middleware serves a Swagger UI (or Scalar / ReDoc) page and the raw -// OpenAPI spec at configurable URL paths. It supports both JSON and YAML -// specs with automatic content-type detection. -// -// Basic usage with an embedded spec: -// -// import "embed" +// Package swagger provides OpenAPI specification viewer middleware for celeris. +// +// [New] returns a [celeris.HandlerFunc] that serves an interactive API +// reference page and the raw OpenAPI spec (JSON or YAML, auto-detected) under +// a configurable base path. By default it registers {BasePath}/ for the UI, +// {BasePath}/spec for the raw spec, and redirects {BasePath} to {BasePath}/; +// other paths and non-GET/HEAD methods pass through or return 405. +// +// [Config] is the entry point. Provide the spec via Config.SpecContent (an +// embedded byte slice) or Config.SpecURL (an externally hosted spec, in which +// case no /spec endpoint is registered). Config.Renderer selects the frontend +// ([RendererSwaggerUI] (default), [RendererScalar], or [RendererReDoc]), and +// Config.Options passes renderer-specific settings as a JSON-serializable map. +// +// [UIConfig] (Config.UI) tunes the Swagger UI renderer — DocExpansion, Title, +// and OAuth2 pre-configuration via [OAuth2Config]; most of its fields are +// ignored by Scalar and ReDoc. Use [IntPtr] to set UIConfig.DefaultModelsExpandDepth, +// which is an *int so an explicit zero is distinguishable from unset. For +// air-gapped deployments, set Config.AssetsPath to serve bundled assets locally +// instead of from a CDN. +// +// The middleware has no built-in authentication; OpenAPI specs may expose +// internal API structure, so place it after auth middleware to protect the +// endpoints. // // //go:embed openapi.json // var spec []byte // -// server.Use(swagger.New(swagger.Config{ -// SpecContent: spec, -// })) -// -// Custom base path and UI options: -// -// server.Use(swagger.New(swagger.Config{ -// SpecContent: spec, -// BasePath: "/docs", -// UI: swagger.UIConfig{ -// DocExpansion: "full", -// DeepLinking: true, -// PersistAuthorization: true, -// DefaultModelsExpandDepth: 1, -// Title: "My API", -// }, -// })) -// -// Using Scalar instead of Swagger UI: -// -// server.Use(swagger.New(swagger.Config{ -// SpecContent: spec, -// Renderer: swagger.RendererScalar, -// })) -// -// Using ReDoc instead of Swagger UI: -// -// server.Use(swagger.New(swagger.Config{ -// SpecContent: spec, -// Renderer: swagger.RendererReDoc, -// })) -// -// Note that [UIConfig] options (DocExpansion, DeepLinking, PersistAuthorization, -// DefaultModelsExpandDepth) apply only to Swagger UI and are ignored when -// Renderer is Scalar or ReDoc. -// -// # Renderer-Specific Options -// -// Use [Config].Options to pass renderer-specific configuration as a -// JSON-serializable map. For ReDoc these are passed to Redoc.init(), -// for Scalar they become the data-configuration attribute, and for -// Swagger UI they are passed to SwaggerUIBundle(). -// -// ReDoc dark theme example: -// -// server.Use(swagger.New(swagger.Config{ -// SpecContent: spec, -// Renderer: swagger.RendererReDoc, -// Options: map[string]any{ -// "theme": map[string]any{ -// "colors": map[string]any{"primary": map[string]any{"main": "#32329f"}}, -// }, -// "expandResponses": "200,201", -// "hideDownloadButton": true, -// }, -// })) -// -// When Options is nil, each renderer uses its own defaults. -// -// # OAuth2 Pre-Configuration -// -// Swagger UI supports pre-filling the OAuth2 authorization dialog. Set -// [UIConfig].OAuth2 to configure client credentials: -// -// server.Use(swagger.New(swagger.Config{ -// SpecContent: spec, -// UI: swagger.UIConfig{ -// OAuth2RedirectURL: "https://example.com/oauth2-redirect", -// OAuth2: &swagger.OAuth2Config{ -// ClientID: "my-client-id", -// AppName: "My Application", -// Scopes: []string{"read:api", "write:api"}, -// UsePKCE: true, -// }, -// }, -// })) -// -// All [OAuth2Config] values are embedded in the HTML page source. Only -// public-client material (ClientID, scopes, app name) is supported; -// confidential client secrets cannot be kept secret in a browser. Use -// PKCE (UsePKCE: true) for the recommended public-client flow. -// -// OAuth2 fields are ignored when Renderer is not [RendererSwaggerUI]. -// -// External spec URL (no /spec endpoint registered): -// -// server.Use(swagger.New(swagger.Config{ -// SpecURL: "https://petstore.swagger.io/v2/swagger.json", -// })) -// -// # Air-Gapped / Self-Hosted Assets -// -// By default, CSS and JavaScript are loaded from public CDNs. For -// environments without internet access, set [Config].AssetsPath to a -// local URL prefix and serve the bundled assets with a static file -// middleware: -// -// server.Use(static.New(static.Config{ -// Root: "./swagger-ui-dist", -// Prefix: "/swagger-assets", -// })) -// server.Use(swagger.New(swagger.Config{ -// SpecContent: spec, -// AssetsPath: "/swagger-assets", -// })) -// -// # Content-Type Detection -// -// When serving the spec via the /spec endpoint, the middleware -// auto-detects whether the content is JSON or YAML. Detection checks -// the [Config].SpecFile extension first (.json, .yaml, .yml), then -// inspects the first non-whitespace byte of the content ('{' or '[' -// indicates JSON; anything else is assumed YAML). -// -// # Endpoints -// -// The middleware registers: -// -// - {BasePath}/ — renders the UI page (HTML) -// - {BasePath}/spec — serves the raw spec (JSON or YAML) -// - {BasePath} — redirects to {BasePath}/ -// -// Requests to other paths pass through. Only GET and HEAD are handled; -// other methods return 405. -// -// # Ordering -// -// The swagger middleware intercepts by path prefix and can be placed at any -// position in the [celeris.Server.Use] chain. Place it after authentication -// middleware if you want to protect the spec and UI endpoints. -// -// # Skipping -// -// Use [Config].Skip for dynamic skip logic or [Config].SkipPaths for -// exact-match path exclusions. Skipped requests call c.Next() without -// serving the UI or spec. +// server.Use(swagger.New(swagger.Config{SpecContent: spec})) // -// # Security +// # Documentation // -// The middleware has no built-in authentication. OpenAPI specs may reveal -// internal API structure. Protect the endpoints with upstream auth middleware -// or network-level controls. When using CDN-loaded assets, ensure your CSP -// policy allows cdn.jsdelivr.net (Scalar) or unpkg.com (Swagger UI) in -// script-src and style-src. +// Full guides and examples: https://goceleris.dev/docs/middleware-content package swagger diff --git a/middleware/timeout/doc.go b/middleware/timeout/doc.go index d83fa2aa..e845a456 100644 --- a/middleware/timeout/doc.go +++ b/middleware/timeout/doc.go @@ -1,54 +1,34 @@ // Package timeout provides request timeout middleware for celeris. // -// The middleware wraps each request's context with a deadline. If the -// downstream handler does not complete before the deadline, the context -// is cancelled and a configurable error response is returned. -// -// Basic usage with the default 5-second timeout: -// -// server.Use(timeout.New()) -// -// Custom timeout and error handler: -// -// server.Use(timeout.New(timeout.Config{ -// Timeout: 10 * time.Second, -// ErrorHandler: func(c *celeris.Context, err error) error { -// return c.JSON(503, map[string]string{"error": "timed out"}) -// }, -// })) -// -// Set [Config].Preemptive to true to run the handler in a separate -// goroutine with response buffering. In preemptive mode, the middleware -// returns the timeout error immediately when the deadline expires, even -// if the handler is blocked on a non-context-aware operation. -// -// Set [Config].TimeoutFunc to compute a timeout dynamically per request. -// The static [Config].Timeout is used as a fallback when TimeoutFunc is -// nil or returns a non-positive duration. -// -// In cooperative mode (default), the handler runs in the request -// goroutine. The context deadline is set, but the handler must check -// c.Context().Done() to respect the timeout. Cooperative mode has no -// extra goroutine overhead and no response buffering cost. -// -// [Config].ErrorHandler receives the triggering error -// (context.DeadlineExceeded, a matched TimeoutErrors entry, or a -// panic-wrapped error). If it panics, [ErrServiceUnavailable] is -// returned as a last-resort fallback. -// -// [Config].TimeoutErrors lists errors that should be treated as -// timeouts even if the deadline has not been reached (e.g., database -// query timeouts). -// -// [ErrServiceUnavailable] is the exported sentinel error (503) returned -// when no custom error handler is configured, usable with errors.Is. -// -// Set [Config].Skip to bypass the middleware dynamically, or -// [Config].SkipPaths for exact-match path exclusions. -// -// Warning: In preemptive mode, if the handler does not exit promptly -// after context cancellation, the middleware goroutine blocks waiting -// for the handler to complete. This ties up the connection and prevents -// the Context from being returned to the pool. Handlers MUST check -// c.Context().Done() and return quickly on cancellation. +// [New] returns a [celeris.HandlerFunc] that wraps each request's context +// with a deadline. If the downstream handler does not complete within the +// configured duration, the context is cancelled and a configurable error +// response is returned. +// +// Key exported symbols: +// +// - [New] — constructs the middleware from a [Config]. +// - [Config] — options: [Config.Timeout] (static duration, default 5 s), +// [Config.TimeoutFunc] (per-request duration; overrides Timeout when > 0), +// [Config.ErrorHandler] (called on timeout; default returns 503), +// [Config.TimeoutErrors] (treat matching upstream errors as timeouts), +// [Config.Preemptive] (run handler in a goroutine; see below), +// [Config.Skip] / [Config.SkipPaths] (bypass the middleware). +// - [ErrServiceUnavailable] — sentinel 503 error; aliases +// celeris.ErrServiceUnavailable for cross-middleware errors.Is matching. +// +// Cooperative mode (default, Preemptive: false): the handler runs in the +// request goroutine with the context deadline set. Handlers must observe +// c.Context().Done() to respect the cancellation. No goroutine or buffer +// overhead. +// +// Preemptive mode (Preemptive: true): the handler runs in a spawned goroutine +// with the response buffered. When the deadline expires the middleware waits +// for the goroutine to exit, discards the buffered response, and invokes the +// error handler. Handlers MUST exit promptly on context cancellation to avoid +// blocking the connection. Incompatible with streaming (StreamWriter). +// +// # Documentation +// +// Full guides and examples: https://goceleris.dev/docs/middleware-traffic package timeout diff --git a/middleware/websocket/doc.go b/middleware/websocket/doc.go index f84919ce..9fa885a0 100644 --- a/middleware/websocket/doc.go +++ b/middleware/websocket/doc.go @@ -1,7 +1,12 @@ // Package websocket provides a zero-dependency native WebSocket middleware // for celeris, implementing RFC 6455. // -// # Basic Echo Server +// Register the middleware on a route with [New] and a [Config] whose +// [Config.Handler] runs once per upgraded connection. The handler receives +// a [*Conn] and should block until the connection is done; it is closed +// automatically when the handler returns. Non-WebSocket requests pass +// through to the next handler, and HTTP/2 requests get 426 Upgrade Required +// (hijacking is impossible over multiplexed streams). // // server.GET("/ws", websocket.New(websocket.Config{ // Handler: func(c *websocket.Conn) { @@ -10,226 +15,41 @@ // if err != nil { // break // } -// if err := c.WriteMessage(mt, msg); err != nil { -// break -// } -// } -// }, -// })) -// -// # Origin Checking -// -// By default, a same-origin check is enforced (the Origin header must -// match the Host header). To allow all origins: -// -// websocket.New(websocket.Config{ -// CheckOrigin: func(c *celeris.Context) bool { return true }, -// Handler: myHandler, -// }) -// -// To restrict to specific origins: -// -// websocket.New(websocket.Config{ -// CheckOrigin: func(c *celeris.Context) bool { -// return c.Header("origin") == "https://example.com" -// }, -// Handler: myHandler, -// }) -// -// # JSON Messaging -// -// websocket.New(websocket.Config{ -// Handler: func(c *websocket.Conn) { -// var msg MyType -// if err := c.ReadJSON(&msg); err != nil { -// return -// } -// c.WriteJSON(msg) -// }, -// }) -// -// # Subprotocol Negotiation -// -// websocket.New(websocket.Config{ -// Subprotocols: []string{"graphql-transport-ws"}, -// Handler: func(c *websocket.Conn) { -// proto := c.Subprotocol() -// // handle based on negotiated protocol -// }, -// }) -// -// # HTTP/2 Limitation -// -// WebSocket requires HTTP/1.1 connection hijacking. HTTP/2 multiplexes -// streams over a single TCP connection, making hijack impossible. This -// middleware returns 426 Upgrade Required for HTTP/2 requests. -// -// # Concurrency -// -// All write methods ([Conn.WriteMessage], [Conn.WriteText], [Conn.WriteBinary], -// [Conn.WriteJSON], [Conn.WritePing]) are internally serialized and safe for -// concurrent use from multiple goroutines. A single goroutine may call -// [Conn.ReadMessage] while others write concurrently. -// -// [Conn.SetPingHandler], [Conn.SetPongHandler], and [Conn.SetCloseHandler] -// must be called before starting the read loop. -// -// # Keepalive (Ping/Pong) -// -// Detect dead connections with periodic pings: -// -// Handler: func(c *websocket.Conn) { -// c.SetPongHandler(func(data []byte) error { -// c.SetReadDeadline(time.Now().Add(60 * time.Second)) -// return nil -// }) -// c.SetReadDeadline(time.Now().Add(60 * time.Second)) -// -// go func() { -// ticker := time.NewTicker(30 * time.Second) -// defer ticker.Stop() -// for range ticker.C { -// if err := c.WritePing(nil); err != nil { -// return -// } +// c.WriteMessage(mt, msg) // } -// }() -// -// for { -// mt, msg, err := c.ReadMessage() -// if err != nil { break } -// c.WriteMessage(mt, msg) -// } -// }, -// -// # Compression (permessage-deflate) -// -// Enable RFC 7692 permessage-deflate compression: -// -// websocket.New(websocket.Config{ -// EnableCompression: true, -// Handler: myHandler, -// }) -// -// Compression is negotiated during the upgrade handshake. Messages above -// the compression threshold (default 128 bytes) are compressed transparently. -// -// # Streaming Large Messages -// -// For large messages, use [Conn.NextReader] and [Conn.NextWriter] to avoid -// buffering the entire message in memory: -// -// mt, reader, _ := c.NextReader() -// io.Copy(dst, reader) -// -// writer, _ := c.NextWriter(websocket.TextMessage) -// writer.Write(chunk1) -// writer.Write(chunk2) -// writer.Close() -// -// # ReadMessage vs ReadMessageReuse -// -// [Conn.ReadMessage] returns an owned copy (safe to retain). -// [Conn.ReadMessageReuse] returns a reused buffer (zero-alloc, only valid -// until the next read call). Use ReadMessageReuse for echo servers and -// message-forwarding proxies where the data is processed immediately. -// -// # Access Request Data -// -// Route params, query params, and headers are captured at upgrade time: -// -// // Route: /ws/:room -// c.Param("room") // route parameter -// c.Query("token") // query parameter -// c.Header("origin") // request header -// -// # Engine-integrated mode vs hijack mode -// -// On the std engine, WebSocket connections are upgraded via Go's standard -// connection hijacking — the handler runs on a goroutine and reads/writes -// directly on a *net.TCPConn. On the native engines (epoll, io_uring), the -// connection stays in the event loop after upgrade: inbound chunks are -// delivered to the handler goroutine via an internal chanReader (which -// applies TCP-level backpressure on overflow), and outbound writes go -// through the engine's per-connection write buffer (cs.writeBuf). -// -// The same Handler signature works for both modes — the middleware picks -// the engine-integrated path automatically when the engine supports it -// and falls back to hijack on the std engine. -// -// # Backpressure semantics -// -// On the engine path, the WebSocket conn maintains a bounded chanReader -// between the event loop and the handler goroutine. When the buffer fills -// past the high-water mark (75% of [Config.MaxBackpressureBuffer]), the -// engine pauses inbound delivery for that connection (epoll: drops -// EPOLLIN; io_uring: cancels the in-flight RECV). The kernel then closes -// the TCP receive window, slowing the peer at the network level. When the -// buffer drains below 25%, the engine resumes inbound delivery. -// -// On the std (hijack) path, backpressure is handled directly by the -// kernel's TCP stack via [net.Conn.Read] returning when the kernel buffer -// has data — no middleware-level buffering happens. -// -// In healthy operation [Conn.BackpressureDropped] returns 0. A non-zero -// value indicates the engine pause/resume mechanism is malfunctioning. -// -// # IdleTimeout semantics -// -// On the std (hijack) path, [Config.IdleTimeout] is enforced via -// [net.Conn.SetReadDeadline], which is reset before each blocking read. -// On the engine path, the WS middleware extends an absolute deadline via -// [Context.SetWSIdleDeadline] after each successful frame read, and the -// engine's idle sweep closes connections whose deadline has expired. -// Both paths converge to the same observable behavior. -// -// # WriteControl deadline semantics -// -// [Conn.WriteControl] applies the supplied deadline to the channel-based -// write semaphore (so a stalled large NextWriter cannot indefinitely block -// pings/pongs). On the std path, the deadline is also pinned to the -// underlying [net.Conn] via [net.Conn.SetWriteDeadline] so a peer that -// has stopped reading cannot stall the actual flush. On the engine path, -// writes go into the engine's write buffer and never block at the -// syscall level — only the lock-acquisition deadline applies. -// -// # Fan-out (Hub) -// -// For broadcasting a single message to N connections, use [Hub] with -// [PreparedMessage]. The frame is encoded once per uncompressed / -// compressed variant and reused across every [Conn.WritePreparedMessage] -// dispatch — so per-message wire-encoding cost is O(1) regardless of -// subscriber count, while per-Conn write throughput remains the -// engine's normal write path. -// -// hub := websocket.NewHub(websocket.HubConfig{ -// OnSlowConn: func(c *websocket.Conn, err error) websocket.HubPolicy { -// return websocket.HubPolicyClose // boot misbehaving peers -// }, -// }) -// server.GET("/ws", websocket.New(websocket.Config{ -// Handler: func(c *websocket.Conn) { -// unregister := hub.Register(c) -// defer unregister() -// // your read loop here // }, // })) -// // Publishers anywhere in the app: -// hub.Broadcast(websocket.TextMessage, []byte(`{"type":"tick"}`)) -// -// Hub.Close drains every in-flight Broadcast (via an internal -// inflight WaitGroup) before tearing down conns, so a shutdown that -// synchronises on Close cannot race a still-fanning-out message. -// -// Authorization MUST happen before [Hub.Register]. Hub broadcasts go -// to every registered connection unfiltered; if a per-conn ACL is -// required, use [Hub.BroadcastFilter] with a pure predicate. -// -// PreparedMessage rejects control opcodes (Ping/Pong/Close) at -// construction time — control frames have RFC 6455 §5.5 size and -// fragmentation constraints that the cache-and-reuse model can't -// satisfy. Use [Conn.WriteControl] per-conn for those. // -// See also: middleware/sse for the equivalent on Server-Sent Events, -// where the same broker pattern lives as [middleware/sse.Broker]. +// On [Conn], [Conn.ReadMessage] / [Conn.WriteMessage] (plus the +// [Conn.WriteText], [Conn.WriteBinary], [Conn.ReadJSON], and [Conn.WriteJSON] +// helpers) cover the common case. [Conn.ReadMessageReuse] returns a buffer +// valid only until the next read for zero-alloc echo/proxy loops, while +// [Conn.NextReader] and [Conn.NextWriter] stream large messages without +// buffering them whole. [Conn.WriteControl], [Conn.WritePing], and the +// [Conn.SetPingHandler] / [Conn.SetPongHandler] / [Conn.SetCloseHandler] +// hooks handle keepalive and shutdown. All write methods are internally +// serialized and safe for concurrent use; a single goroutine may read while +// others write. +// +// [Config] options control origin checking ([Config.CheckOrigin], default +// same-origin), subprotocol negotiation ([Config.Subprotocols]), +// permessage-deflate compression ([Config.EnableCompression], RFC 7692), +// idle timeouts, and backpressure tuning for the native-engine path. Route +// params, query values, and headers captured at upgrade time are available +// via [Conn.Param], [Conn.Query], and [Conn.Header]. +// +// The same Handler works on every engine. On the std engine the connection +// is hijacked for direct I/O; on native engines (epoll, io_uring) it stays +// in the event loop with TCP-level backpressure. [Conn.BackpressureDropped] +// returns 0 in healthy operation. +// +// For broadcasting one message to many connections, register each [*Conn] +// with a [Hub] and publish via [Hub.Broadcast], [Hub.BroadcastFilter], or +// [Hub.BroadcastPrepared] with a [PreparedMessage] (encoded once, reused +// across subscribers). Slow connections are handled by [HubConfig.OnSlowConn] +// returning a [HubPolicy]. Authorization must happen before [Hub.Register]. +// +// # Documentation +// +// Full guides and examples: https://goceleris.dev/docs/websocket package websocket diff --git a/observe/doc.go b/observe/doc.go index 0b16932f..c73ec92b 100644 --- a/observe/doc.go +++ b/observe/doc.go @@ -7,4 +7,8 @@ // // For Prometheus and debug endpoint integration, see the // middleware/metrics package. +// +// # Documentation +// +// Full guides and examples: https://goceleris.dev/docs/observability package observe diff --git a/probe/doc.go b/probe/doc.go index e14c8e21..c701f957 100644 --- a/probe/doc.go +++ b/probe/doc.go @@ -1,9 +1,15 @@ -// Package probe provides capability detection for io_uring and epoll. +// Package probe provides kernel capability detection for the io_uring engine. // -// At engine startup, the celeris io_uring engine calls Detect() to discover -// which kernel features are available (multishot accept/recv, provided -// buffer rings, fixed-file tables, SEND_ZC, etc.) and assigns a Tier from -// the resulting Capabilities. Application code typically does not call -// this package directly; inspect [celeris.Server.EngineInfo] at runtime -// for the active feature set. +// At engine startup, [Probe] (or [ProbeWith] for testing) discovers which +// kernel features are available — multishot accept/recv, provided buffer +// rings, fixed-file tables, SEND_ZC, SQPOLL, and more — and returns an +// [engine.CapabilityProfile] that includes the selected [engine.Tier]. +// Application code typically does not call this package directly; inspect +// [celeris.Server.EngineInfo] at runtime for the active feature set. +// [DiagnosticReport] and [FormatDiagnostic] can surface the profile as +// human-readable text for troubleshooting. +// +// # Documentation +// +// Full guides and examples: https://goceleris.dev/docs/engines package probe diff --git a/protocol/h1/doc.go b/protocol/h1/doc.go index 403354e2..b65b35d9 100644 --- a/protocol/h1/doc.go +++ b/protocol/h1/doc.go @@ -6,4 +6,8 @@ // that alias the connection's read buffer — callers must materialize // (clone) any value they retain past the next [ParseRequest] call on the // same connection. +// +// # Documentation +// +// Full guides and examples: https://goceleris.dev/docs/engines package h1 diff --git a/protocol/h2/doc.go b/protocol/h2/doc.go index 035d1960..2f36e2f2 100644 --- a/protocol/h2/doc.go +++ b/protocol/h2/doc.go @@ -9,4 +9,8 @@ // // Application code should not import these packages directly — interact // with HTTP/2 through the celeris [Server] and [Context] types. +// +// # Documentation +// +// Full guides and examples: https://goceleris.dev/docs/engines package h2 diff --git a/resource/doc.go b/resource/doc.go index 11580ccf..08322e94 100644 --- a/resource/doc.go +++ b/resource/doc.go @@ -1,4 +1,21 @@ // Package resource defines server configuration, resource limits, and default // presets. The top-level celeris.Config is the primary user-facing type; this // package provides the internal representation used by engine implementations. +// +// Key exported symbols: +// - [Config] — internal server configuration with [Config.Validate] and +// [Config.WithDefaults] helpers used by engine implementations. +// - [Resources] — optional user overrides for buffer counts and concurrency +// limits; zero values fall back to engine defaults. +// - [ResolvedResources] — the fully resolved values after applying defaults, +// user overrides, and hard caps; consumed by engines at startup. +// - [WorkloadHint] — optional operator hint ([WorkloadLowConcurrency] / +// [WorkloadHighConcurrency]) +// that influences which I/O backend the adaptive engine starts on. +// - [NextPowerOf2] — utility used by engine packages to size io_uring rings +// and similar power-of-two kernel structures. +// +// # Documentation +// +// Full guides and examples: https://goceleris.dev/docs/configuration package resource diff --git a/server.go b/server.go index 515f1e99..b379aa94 100644 --- a/server.go +++ b/server.go @@ -538,8 +538,9 @@ func (s *Server) ResumeAccept() error { return ac.ResumeAccept() } -// Collector returns the metrics collector, or nil if the server has not been -// started or if Config.DisableMetrics is true. +// Collector returns the metrics collector. It is created eagerly in New, so it +// is non-nil before Start is called; it returns nil only when +// Config.DisableMetrics is true. func (s *Server) Collector() *observe.Collector { return s.collector } diff --git a/test/perfmatrix/doc.go b/test/perfmatrix/doc.go index e6bfe8d5..8ba898f6 100644 --- a/test/perfmatrix/doc.go +++ b/test/perfmatrix/doc.go @@ -15,29 +15,13 @@ // report/ csv / markdown / benchstat / pprof index writers // profiling/ per-cell pprof capture helpers // -// # Running -// -// Invoke the mage targets from the repo root: -// -// mage matrixBench # full release-gate (10 × 10s), ~2.5 days -// mage matrixBenchDeep # maximum-rigor (10 × 15s, 3s warmup), ~3.5 days -// mage matrixBenchStrict # -race + checkptr + fail-fast (3 × 5s), ~4-8h -// mage matrixBenchQuick # dev-loop (3 × 5s, static only), ~1 hour -// mage matrixBenchDrivers # driver cells only (10 × 10s) -// mage matrixBenchProfile # full matrix with pprof capture per cell -// mage matrixBenchSince # HEAD vs PERFMATRIX_REF, fails on >2% regression -// -// matrixBenchStrict is the release-gate confidence check. The runner -// and every in-process engine / server are built with -race and -// -gcflags=-d=checkptr=2; GORACE=halt_on_error=1 + GOTRACEBACK=crash -// + runner -fail-fast abort the moment any data race, use-after-free, -// or invalid unsafe.Pointer conversion surfaces — the bug class that -// otherwise sits buried for hours of churn load before the consequence -// fires. -// // # Cell identifiers // // Each cell is uniquely addressed by "/". // Server names follow "-[-upgrade][+async]"; see // servers/celeris for the full celeris naming scheme. +// +// # Documentation +// +// Full guides and examples: https://goceleris.dev/docs package perfmatrix diff --git a/test/redisspec/doc.go b/test/redisspec/doc.go index b723b5c4..9bf67b01 100644 --- a/test/redisspec/doc.go +++ b/test/redisspec/doc.go @@ -14,21 +14,12 @@ // - Section 7: Wire edge cases (split reads, flooding, binary keys, concurrency) // - Section 8: Protocol fuzzing (fuzz_test.go) // -// # Running -// -// The tests are gated by the `redisspec` build tag and the CELERIS_REDIS_ADDR -// environment variable. Both must be present for any test to execute: -// -// CELERIS_REDIS_ADDR='127.0.0.1:6379' \ -// go test -tags redisspec -count=1 -timeout=300s -v ./test/redisspec/... -// -// A default Redis 7.2 instance with no authentication is sufficient for all -// tests except the AUTH section, which requires CELERIS_REDIS_PASSWORD. +// Tests are gated by the `redisspec` build tag and the CELERIS_REDIS_ADDR +// environment variable. Every test uses raw TCP connections and the +// [github.com/goceleris/celeris/driver/redis/protocol] package for RESP +// encoding/decoding — no third-party Redis client is imported. // -// # Design +// # Documentation // -// Every test uses raw TCP connections and the celeris -// [github.com/goceleris/celeris/driver/redis/protocol] package for RESP -// encoding/decoding. No third-party Redis client library is imported. This -// isolates the verification to pure protocol-level correctness. +// Full guides and examples: https://goceleris.dev/docs package redisspec diff --git a/validation/doc.go b/validation/doc.go index c439f39f..0ad4734f 100644 --- a/validation/doc.go +++ b/validation/doc.go @@ -5,31 +5,21 @@ // no-op stubs in disabled.go, so the counters and the unix-socket // endpoint are stripped at compile time — no allocations, no atomic // adds, no goroutines. Validation builds (-tags=validation) compile -// against assertions.go and endpoint.go, exposing the counters as -// atomic.Uint64 and serving a JSON snapshot over -// /tmp/celeris-validation.sock. +// against assertions.go and endpoint.go, exposing named [Counter] +// variables (e.g. [PanicCount], [RatelimitTokenViolations]) as +// atomic.Uint64 and serving a JSON snapshot over [SocketPath] via +// [StartEndpoint]. // -// External property-test harnesses read the socket on every poll to -// feed property predicates. The counter shape is stable across both -// build modes via the [Counters] struct and the [Snapshot] function — -// the only thing that changes is whether reads return live atomics -// or the zero value. +// [Snapshot] returns a [Counters] struct whose shape is stable across +// both build modes — reads return live atomics under -tags=validation +// and zero values otherwise. External property-test harnesses poll +// [Snapshot] or read [SocketPath] on every tick to feed predicates. // -// # Call-pattern convention +// Unconditional event counts (e.g. recovered panics) are recorded via +// thin helpers such as [RecordPanic]; these carry no build tag and +// inline to nothing in production builds. // -// Two call patterns coexist and SHOULD be used consistently: +// # Documentation // -// - For unconditional event counts (panic recovered, etc.) call the -// RecordX() helper defined in hooks.go (e.g. validation.RecordPanic()). -// Helpers carry no build tag — they delegate to Counter.Add, which -// inlines to nothing under !validation. -// - For predicate-bearing checks (a value violates an invariant) call -// the per-package validate*() helper defined in that package's -// validation_check.go (under //go:build validation) with a no-op -// stub in validation_default.go (under //go:build !validation). -// Examples: middleware/jwt.validateAdmission, -// middleware/ratelimit.validateBucket. -// -// Bare validation.Counter.Add(1) outside a validate*() helper is a -// bug — use a RecordX() helper instead. +// Full guides and examples: https://goceleris.dev/docs package validation