Tags: goceleris/celeris
Tags
fix: post-merge CI flakes + iouring detached close race (#231) * test(websocket): TestWriteBufferPool handler-done sync Fixes a latent flake that surfaced on GitHub Actions right after the v1.3.4 merge to main: --- FAIL: TestWriteBufferPool (0.01s) websocket_test.go:1747: pool.Put was never called The test checked getCalls/putCalls immediately after client.readServerFrame, but the server's WriteMessage defer chain (unlockWrite → putWriter) runs AFTER bw.Flush returns to the client. On a fast local host the defers land microseconds before the counter check; on a slow CI runner the client wins the race and reads putCalls == 0 before the server's defer stack unwinds. Same shape as the TestWriteBufferPoolHijackOnly fix in efebbba — attach a handlerDone channel that closes after the user handler function returns (guaranteed after WriteMessage's defers have run), and have the test wait on it before reading the pool counters. Bounded with a 2s timeout so a genuine regression still surfaces as a test failure, not a deadlock. Verified with `go test -race -count=500 -run TestWriteBufferPool$`. * fix(iouring): defer detached close until pending SENDs complete CI on kernel 6.17 (iouring tier=optional) caught this regression after PR #229 shipped: --- FAIL: TestNativeEngineCloseFrameEcho/io_uring/7.7.9-1011 closeframe_engine_linux_test.go:68: read frame header: EOF The WS middleware's close path queues a close-frame echo via the guarded writeFn, then asks the engine to drop the FD through SetWSIdleDeadline(1). On epoll that's safe — checkTimeouts fires only AFTER drainDetachQueue + the synchronous unix.Write pass in the same loop iteration, so a tiny close frame is always in the kernel send buffer before SHUT_WR. On iouring, flushSend submits a SEND SQE that the kernel completes asynchronously: closeConn could land between SQE submission and completion and tear the FD down mid-send, so the echo never hit the wire. Non-detached paths already defer via `cs.closing = true` when sending/sendBuf/writeBuf are non-empty. Move that check above the detached branch so WS/SSE connections get the same deferred-close semantics, then dispatch the eventual close through a small finishCloseAny helper that picks finishCloseDetached vs finishClose based on cs.detachMu — the detached variant does NOT return the connState to the pool because goroutine closures still hold references. Sibling callsites in handleSend and completeSend (three places that fire when cs.closing is true and buffers drain) now also route through finishCloseAny, so a detached conn that hits a SEND error mid-close-handshake still unwinds cleanly without leaking a pooled connState entry back into the H1/H2 path. Local repro impossible on Docker Desktop (kernel 6.12 = iouring tier=none). CI will exercise the fix on the 6.17 runner. * test(integration): retry loop for TestAdaptiveConstrainedRing Same root cause and fix shape as TestAdaptiveAutoSingleWorker (commits 88c92b2 + f912fd4): Azure CI VMs hit transient ACCEPT_DIRECT EINVAL during io_uring engine stabilization, which the adaptive engine surfaces as a connection-reset to the first client request. The test's single Get was bound to a 3s timeout with no retry, so any stabilization blip failed the build: adaptive_hybrid_test.go:347: request failed: Get "http://127.0.0.1:...:/constrained": read: connection reset by peer Wrap the request in a 5-second deadline + 100ms-backoff retry loop (identical shape to the sibling tests) and disable client-side keep- alive so a previously reset pooled connection cannot poison the retry. A genuine handler-side regression still fails after 5 seconds of retries; this only absorbs the one-shot startup race.
fix: post-merge CI flakes + iouring detached close race (#231) * test(websocket): TestWriteBufferPool handler-done sync Fixes a latent flake that surfaced on GitHub Actions right after the v1.3.4 merge to main: --- FAIL: TestWriteBufferPool (0.01s) websocket_test.go:1747: pool.Put was never called The test checked getCalls/putCalls immediately after client.readServerFrame, but the server's WriteMessage defer chain (unlockWrite → putWriter) runs AFTER bw.Flush returns to the client. On a fast local host the defers land microseconds before the counter check; on a slow CI runner the client wins the race and reads putCalls == 0 before the server's defer stack unwinds. Same shape as the TestWriteBufferPoolHijackOnly fix in efebbba — attach a handlerDone channel that closes after the user handler function returns (guaranteed after WriteMessage's defers have run), and have the test wait on it before reading the pool counters. Bounded with a 2s timeout so a genuine regression still surfaces as a test failure, not a deadlock. Verified with `go test -race -count=500 -run TestWriteBufferPool$`. * fix(iouring): defer detached close until pending SENDs complete CI on kernel 6.17 (iouring tier=optional) caught this regression after PR #229 shipped: --- FAIL: TestNativeEngineCloseFrameEcho/io_uring/7.7.9-1011 closeframe_engine_linux_test.go:68: read frame header: EOF The WS middleware's close path queues a close-frame echo via the guarded writeFn, then asks the engine to drop the FD through SetWSIdleDeadline(1). On epoll that's safe — checkTimeouts fires only AFTER drainDetachQueue + the synchronous unix.Write pass in the same loop iteration, so a tiny close frame is always in the kernel send buffer before SHUT_WR. On iouring, flushSend submits a SEND SQE that the kernel completes asynchronously: closeConn could land between SQE submission and completion and tear the FD down mid-send, so the echo never hit the wire. Non-detached paths already defer via `cs.closing = true` when sending/sendBuf/writeBuf are non-empty. Move that check above the detached branch so WS/SSE connections get the same deferred-close semantics, then dispatch the eventual close through a small finishCloseAny helper that picks finishCloseDetached vs finishClose based on cs.detachMu — the detached variant does NOT return the connState to the pool because goroutine closures still hold references. Sibling callsites in handleSend and completeSend (three places that fire when cs.closing is true and buffers drain) now also route through finishCloseAny, so a detached conn that hits a SEND error mid-close-handshake still unwinds cleanly without leaking a pooled connState entry back into the H1/H2 path. Local repro impossible on Docker Desktop (kernel 6.12 = iouring tier=none). CI will exercise the fix on the 6.17 runner. * test(integration): retry loop for TestAdaptiveConstrainedRing Same root cause and fix shape as TestAdaptiveAutoSingleWorker (commits 88c92b2 + f912fd4): Azure CI VMs hit transient ACCEPT_DIRECT EINVAL during io_uring engine stabilization, which the adaptive engine surfaces as a connection-reset to the first client request. The test's single Get was bound to a 3s timeout with no retry, so any stabilization blip failed the build: adaptive_hybrid_test.go:347: request failed: Get "http://127.0.0.1:...:/constrained": read: connection reset by peer Wrap the request in a 5-second deadline + 100ms-backoff retry loop (identical shape to the sibling tests) and disable client-side keep- alive so a previously reset pooled connection cannot poison the retry. A genuine handler-side regression still fails after 5 seconds of retries; this only absorbs the one-shot startup race.
fix: post-merge CI flakes + iouring detached close race (#231) * test(websocket): TestWriteBufferPool handler-done sync Fixes a latent flake that surfaced on GitHub Actions right after the v1.3.4 merge to main: --- FAIL: TestWriteBufferPool (0.01s) websocket_test.go:1747: pool.Put was never called The test checked getCalls/putCalls immediately after client.readServerFrame, but the server's WriteMessage defer chain (unlockWrite → putWriter) runs AFTER bw.Flush returns to the client. On a fast local host the defers land microseconds before the counter check; on a slow CI runner the client wins the race and reads putCalls == 0 before the server's defer stack unwinds. Same shape as the TestWriteBufferPoolHijackOnly fix in efebbba — attach a handlerDone channel that closes after the user handler function returns (guaranteed after WriteMessage's defers have run), and have the test wait on it before reading the pool counters. Bounded with a 2s timeout so a genuine regression still surfaces as a test failure, not a deadlock. Verified with `go test -race -count=500 -run TestWriteBufferPool$`. * fix(iouring): defer detached close until pending SENDs complete CI on kernel 6.17 (iouring tier=optional) caught this regression after PR #229 shipped: --- FAIL: TestNativeEngineCloseFrameEcho/io_uring/7.7.9-1011 closeframe_engine_linux_test.go:68: read frame header: EOF The WS middleware's close path queues a close-frame echo via the guarded writeFn, then asks the engine to drop the FD through SetWSIdleDeadline(1). On epoll that's safe — checkTimeouts fires only AFTER drainDetachQueue + the synchronous unix.Write pass in the same loop iteration, so a tiny close frame is always in the kernel send buffer before SHUT_WR. On iouring, flushSend submits a SEND SQE that the kernel completes asynchronously: closeConn could land between SQE submission and completion and tear the FD down mid-send, so the echo never hit the wire. Non-detached paths already defer via `cs.closing = true` when sending/sendBuf/writeBuf are non-empty. Move that check above the detached branch so WS/SSE connections get the same deferred-close semantics, then dispatch the eventual close through a small finishCloseAny helper that picks finishCloseDetached vs finishClose based on cs.detachMu — the detached variant does NOT return the connState to the pool because goroutine closures still hold references. Sibling callsites in handleSend and completeSend (three places that fire when cs.closing is true and buffers drain) now also route through finishCloseAny, so a detached conn that hits a SEND error mid-close-handshake still unwinds cleanly without leaking a pooled connState entry back into the H1/H2 path. Local repro impossible on Docker Desktop (kernel 6.12 = iouring tier=none). CI will exercise the fix on the 6.17 runner. * test(integration): retry loop for TestAdaptiveConstrainedRing Same root cause and fix shape as TestAdaptiveAutoSingleWorker (commits 88c92b2 + f912fd4): Azure CI VMs hit transient ACCEPT_DIRECT EINVAL during io_uring engine stabilization, which the adaptive engine surfaces as a connection-reset to the first client request. The test's single Get was bound to a 3s timeout with no retry, so any stabilization blip failed the build: adaptive_hybrid_test.go:347: request failed: Get "http://127.0.0.1:...:/constrained": read: connection reset by peer Wrap the request in a 5-second deadline + 100ms-backoff retry loop (identical shape to the sibling tests) and disable client-side keep- alive so a previously reset pooled connection cannot poison the retry. A genuine handler-side regression still fails after 5 seconds of retries; this only absorbs the one-shot startup race.
fix: post-merge CI flakes + iouring detached close race (#231) * test(websocket): TestWriteBufferPool handler-done sync Fixes a latent flake that surfaced on GitHub Actions right after the v1.3.4 merge to main: --- FAIL: TestWriteBufferPool (0.01s) websocket_test.go:1747: pool.Put was never called The test checked getCalls/putCalls immediately after client.readServerFrame, but the server's WriteMessage defer chain (unlockWrite → putWriter) runs AFTER bw.Flush returns to the client. On a fast local host the defers land microseconds before the counter check; on a slow CI runner the client wins the race and reads putCalls == 0 before the server's defer stack unwinds. Same shape as the TestWriteBufferPoolHijackOnly fix in efebbba — attach a handlerDone channel that closes after the user handler function returns (guaranteed after WriteMessage's defers have run), and have the test wait on it before reading the pool counters. Bounded with a 2s timeout so a genuine regression still surfaces as a test failure, not a deadlock. Verified with `go test -race -count=500 -run TestWriteBufferPool$`. * fix(iouring): defer detached close until pending SENDs complete CI on kernel 6.17 (iouring tier=optional) caught this regression after PR #229 shipped: --- FAIL: TestNativeEngineCloseFrameEcho/io_uring/7.7.9-1011 closeframe_engine_linux_test.go:68: read frame header: EOF The WS middleware's close path queues a close-frame echo via the guarded writeFn, then asks the engine to drop the FD through SetWSIdleDeadline(1). On epoll that's safe — checkTimeouts fires only AFTER drainDetachQueue + the synchronous unix.Write pass in the same loop iteration, so a tiny close frame is always in the kernel send buffer before SHUT_WR. On iouring, flushSend submits a SEND SQE that the kernel completes asynchronously: closeConn could land between SQE submission and completion and tear the FD down mid-send, so the echo never hit the wire. Non-detached paths already defer via `cs.closing = true` when sending/sendBuf/writeBuf are non-empty. Move that check above the detached branch so WS/SSE connections get the same deferred-close semantics, then dispatch the eventual close through a small finishCloseAny helper that picks finishCloseDetached vs finishClose based on cs.detachMu — the detached variant does NOT return the connState to the pool because goroutine closures still hold references. Sibling callsites in handleSend and completeSend (three places that fire when cs.closing is true and buffers drain) now also route through finishCloseAny, so a detached conn that hits a SEND error mid-close-handshake still unwinds cleanly without leaking a pooled connState entry back into the H1/H2 path. Local repro impossible on Docker Desktop (kernel 6.12 = iouring tier=none). CI will exercise the fix on the 6.17 runner. * test(integration): retry loop for TestAdaptiveConstrainedRing Same root cause and fix shape as TestAdaptiveAutoSingleWorker (commits 88c92b2 + f912fd4): Azure CI VMs hit transient ACCEPT_DIRECT EINVAL during io_uring engine stabilization, which the adaptive engine surfaces as a connection-reset to the first client request. The test's single Get was bound to a 3s timeout with no retry, so any stabilization blip failed the build: adaptive_hybrid_test.go:347: request failed: Get "http://127.0.0.1:...:/constrained": read: connection reset by peer Wrap the request in a 5-second deadline + 100ms-backoff retry loop (identical shape to the sibling tests) and disable client-side keep- alive so a previously reset pooled connection cannot poison the retry. A genuine handler-side regression still fails after 5 seconds of retries; this only absorbs the one-shot startup race.
feat: v1.3.3 utility & serving middleware — pprof, swagger, static, r… …ewrite, adapters (#223) * feat: add pprof, swagger, static, rewrite, and adapters middleware Five new utility & serving middleware for v1.3.3: - pprof (#189): Go profiling endpoints with loopback-only default. Wraps all net/http/pprof handlers via celeris.Adapt(). AuthFunc guard. 10 tests. - swagger (#190): OpenAPI spec + CDN-loaded Swagger UI/Scalar. No bundled assets. SpecContent or fs.FS support. AuthFunc optional. 16 tests. - static (#191): Static file serving with SPA mode, Cache-Control, prefix stripping, directory browse. Path traversal protection via FileFromDir (OS) and path.Clean + .. rejection (fs.FS). 20 tests. - rewrite (#192): Regex-based URL rewriting with capture groups ($1/$2). Pre-routing (Server.Pre). Compile at init, sorted first-match-wins. Optional redirect mode (301/302). 9 tests. - adapters (#193): Bidirectional stdlib ↔ celeris conversion. WrapMiddleware wraps func(http.Handler) http.Handler for celeris chains. ToStdlib delegates to celeris.ToHandler. ReverseProxy via httputil. 8 tests. Documentation: - middleware/doc.go: ordering guide updated (rewrite in Pre, pprof/swagger/static in Use) - README.md: 5 new rows, package count 29 - SECURITY.md: v1.3.3 section (pprof loopback, static traversal, swagger AuthFunc) - CONTRIBUTING.md: new reference implementation categories Closes #189, Closes #190, Closes #191, Closes #192, Closes #193 * fix: resolve all review findings from v1.3.3 audit Security: - swagger: escape SpecURL with html.EscapeString before injecting into HTML/JS templates (prevents XSS via misconfigured SpecURL) Code quality: - pprof: replace manual skipMap with celeris.SkipHelper (per CONTRIBUTING.md) - adapters: fix gofmt formatting + errcheck for resp.Body.Close - .golangci.yml: add pprof var-naming exclusion (package name matches stdlib) * fix: resolve all remaining review findings - swagger/doc.go: add CSP warning for secure middleware interaction (cdn.jsdelivr.net must be allowed in script-src/style-src) - rewrite/doc.go: anchor regex in basic example (^/old$ not /old), add Security section with ReDoS and open redirect warnings - adapters/adapters.go: add 100MB body size cap to responseCapture (prevents OOM from misbehaving stdlib middleware, matches core bridge) * fix: major rework of all 5 middleware to address review findings static: - FIX: Browse XSS — href uses url.PathEscape + ./ prefix (prevents javascript:) - FIX: Prefix matching now segment-boundary-aware (/api no longer matches /api-docs) - ADD: ETag/Last-Modified headers on OS and fs.FS files (conditional GET + 304) - ADD: Range requests (206) for fs.FS files (video seeking, download resume) - ADD: accept-ranges: bytes header - 28 tests covering all new functionality swagger: - FIX: YAML spec auto-detected and served with correct Content-Type - ADD: UIConfig with DocExpansion, DeepLinking, PersistAuthorization, Title - ADD: AssetsPath for air-gapped/self-hosted asset serving (no CDN required) - ADD: RendererScalar/RendererSwaggerUI type for UI engine selection - 26 tests adapters: - FIX: WrapMiddleware now propagates pre-inner headers (rs/cors, gorilla/csrf work) - FIX: buildRequest sets ContentLength, TLS state, Protocol version - ADD: ReverseProxy with Options (WithTransport, WithModifyRequest, WithErrorHandler) - ADD: Pooled responseCapture for zero-alloc hot path - DEL: ToStdlib (was pure alias of celeris.ToHandler) - DOC: Limitations section (no Hijacker/Flusher for WebSocket/streaming) rewrite: - FIX: Rules changed from map[string]string to []Rule slice (insertion-order, not alphabetical — matches user expectations and Echo's approach) - DOC: First-Match-Wins section updated for ordered evaluation * fix: gofmt formatting * fix: security fix + integration review fixes for v1.3.3 middleware Security: - static: fix path traversal in Browse directory listing. serveOS now validates that filepath.Join result stays within root directory (prefix check + symlink escape check), mirroring FileFromDir. Added TestPathTraversalBrowse and TestPathTraversalFile. Core integration: - rewrite: make New() variadic (config ...Config) with defaultConfig and applyDefaults() for pattern consistency with all other middleware - pprof: use cfg := defaultConfig instead of var cfg Config - swagger: use c.Redirect() instead of manual SetHeader + NoContent Tests: - pprof: add TestPprofSkipFunc (was missing Skip callback coverage) - adapters: add TestReverseProxyPanicNilTarget and TestReverseProxyOptions (ReverseProxy had zero test coverage) - rewrite/bench_test.go: fix regex (d+) -> (\d+) so BenchmarkRewriteMatch actually tests the match path Benchmarks: - bench_chain_test.go: add BenchmarkChainPreRoutingWithRewrite, BenchmarkChainRewritePassthrough, BenchmarkChainWithWrapMiddleware - swagger/bench_test.go: fix spec path /swagger/doc.json -> /swagger/spec Documentation: - middleware/doc.go: fix rewrite production stack example ([]Rule syntax), fix swagger description (remove AuthFunc claim), add adapters mention - pprof/swagger/static doc.go: add missing Skip/Ordering sections - adapters doc.go: document ReverseProxy function and Option API - SECURITY.md: fix swagger AuthFunc claim (feature doesn't exist) - README.md: fix static description (no SPA or Cache-Control) * fix: address all 33-agent review findings for v1.3.3 middleware P0 fixes: - adapters: add 100MB body size cap to responseCapture.Write (matches core bridge maxBridgeResponseBytes). Prevents OOM from misbehaving stdlib middleware. - adapters: WrapMiddleware(nil) now panics at init instead of deferred panic on first request. - static: validate() rejects Prefix without leading / (was silently serving nothing). Feature additions: - static: SPA mode (Config.SPA bool) — serves index file for non-existent paths instead of falling through. Works for both OS and fs.FS backends. - static: Cache-Control (Config.MaxAge time.Duration) — sets "public, max-age=N" header alongside ETag/Last-Modified. - swagger: ReDoc renderer (RendererReDoc) with CDN + self-hosted assets support. - swagger: DefaultModelsExpandDepth changed to *int to allow depth=0 (zero-value sentinel collision fixed). - rewrite: per-rule RedirectCode (Rule.RedirectCode) — each rule can override the config-level default (e.g., /old→301, /temp→307). Code quality: - static: precompute filepath.Clean(root) in New() (saves 1 alloc/req) - static: deduplicate ETag computation (setCacheHeaders returns etag, notModified accepts it instead of recomputing) - static: io.ReadAll → make([]byte, size) + io.ReadFull for fs.FS (eliminates O(log N) growth allocations) - static: fix If-None-Match to handle comma-separated ETag lists (RFC 7232 weak comparison with W/ prefix stripping) - static: fix RFC 7232 §6 — skip If-Modified-Since when If-None-Match is present (regardless of match result) - pprof/swagger: use NewHTTPError() for 403/404/405 instead of c.NoContent() (enables global error handler integration) - swagger: HTML-escape Title and AssetsPath in templates - swagger: pin Scalar CDN to @1 major version - rewrite: validate empty Rule.Pattern (panics instead of silently matching everything) Tests (30+ new): - static: SPA mode (OS + FS + disabled), MaxAge, HEAD fs.FS, IMS 304 fs.FS, multi-ETag If-None-Match, Prefix validation panic - swagger: HEAD on /spec, DocExpansion "none", ReDoc renderer + title + assets, DefaultModelsExpandDepth=0 - adapters: WrapMiddleware nil panic, query string round-trip, host propagation, body round-trip, responseCapture body cap, ReverseProxy behavioral (httptest.Server), ReverseProxy error handler - pprof: IPv6 loopback (allow ::1 + deny remote v6), bare prefix, validate Prefix="/" panic - rewrite: per-rule RedirectCode, empty Pattern panic, invalid per-rule RedirectCode panic Benchmarks: - bench_chain_test.go: add BenchmarkChainPprofPassthrough, BenchmarkChainSwaggerPassthrough, BenchmarkChainStaticPassthrough, BenchmarkChainStaticServe (with etag) Documentation: - pprof: fix SkipPaths doc (was "before any other logic", now correctly says "before auth and handler dispatch") - pprof: validate Prefix="/" (would intercept all traffic) - pprof: rename ExampleNew_publicAccess → ExampleNew_tokenAuth, add actual ExampleNew_publicAccess - swagger: doc.go notes UIConfig options are Swagger UI-only - static: doc.go adds SPA Mode + Cache-Control sections - rewrite: Rule.Pattern doc recommends ^ and $ anchors - adapters: doc.go warns about dual CORS + streaming limitation - middleware/doc.go: Pre-routing ordering guidance * fix: goimports ordering + errcheck in lint * feat: address all remaining weakness and feature gaps static: - io.ReadSeeker fast-path: when fs.File implements ReadSeeker, range requests seek to offset and read only the requested bytes instead of loading the entire file into memory. Eliminates 50MB heap allocation for range requests on large files. - Content-type sniffing: when mime.TypeByExtension fails, read first 512 bytes and use http.DetectContentType instead of falling back to application/octet-stream. - Pre-compressed file support (Config.Compress): checks for .br and .gz variants when Accept-Encoding allows, sets Content-Encoding and Vary headers. Brotli preferred over gzip. OS filesystem only. - 7 new tests: ReadSeeker, content-type sniffing (OS+FS+ReadSeeker), pre-compressed (brotli, gzip, not accepted, missing). swagger: - Exported IntPtr helper for DefaultModelsExpandDepth ergonomics. - ReDoc customization: ReDocConfig struct with Theme (dark mode), ExpandResponses, HideDownloadButton, ScrollYOffset, NoAutoAuth. Dark theme maps to full ReDoc color scheme. - OAuth2 UI pre-configuration: OAuth2Config struct with ClientID, ClientSecret, Realm, AppName, Scopes. Generates ui.initOAuth() call in Swagger UI. OAuth2RedirectURL support. - 12 new tests: IntPtr, ReDoc theme/expandResponses/hideDownload/ scrollOffset/noAutoAuth/defaults, OAuth2 full/nil/partial/redirectURL. adapters: - WithModifyResponse option for ReverseProxy: modify response headers from backend before forwarding to client. - http.Flusher support on responseCapture (no-op): prevents panics in stdlib middleware that type-asserts Flusher. Hijacker still unsupported. - Performance section in doc.go: explicitly documents 8-15 alloc cost and recommends native middleware for hot paths. - 3 new tests: ModifyResponse, Flusher type assertion, WrapMiddleware with Flusher. rewrite: - Conditional rewriting: Rule.Methods restricts to specific HTTP methods, Rule.Host restricts to specific Host header value. Methods map built at init time (zero per-request alloc). Both empty means match all. - 4 new tests: method restriction, host restriction, combined, no restriction (backwards compatibility). * fix: gofmt static_test.go * refactor: simplify APIs, fix Flusher footgun, extract static helpers swagger API simplification: - Delete ReDocConfig struct — replaced by generic Options map[string]any that works for ALL renderers (ReDoc, Scalar, Swagger UI). More powerful with fewer types. Users pass raw renderer options as JSON-serializable maps. - Delete IntPtr helper — revert DefaultModelsExpandDepth to plain int. Zero-value (0 = show model names) is a reasonable default. Document clearly. - Add ClientSecret security warning to OAuth2Config doc: values are embedded in page source, only use for dev/test or PKCE public clients. - Net: 3 exported structs (was 4), 0 helper functions (was 1). static refactoring (no behavior changes): - Extract detectContentType() — extension check + http.DetectContentType - Extract sniffContentType() — ReadSeeker-specific with seek-back - Extract serveRangeFromReader() — range handling from io.ReadSeeker - Extract serveFSFullRead() — non-ReadSeeker full-read path - Extract servePreCompressedFS() — pre-compressed file support for fs.FS (was OS-only, now works with embed.FS and custom fs.FS) - serveFS is now a clean orchestrator calling focused helpers - Update Compress doc: works with both OS and fs.FS adapters Flusher fix: - Remove no-op Flush() from responseCapture. A silent no-op is worse than not implementing the interface: SSE middleware calls Flush() expecting data delivery, gets silent success, client receives nothing until response completes. Now the type assertion returns false, so stdlib middleware can take appropriate fallback action. - Remove Unwrap() (returning nil was misleading for ResponseController) - Update doc.go Limitations section * fix: errcheck on rs.Seek in sniffContentType * fix: final polish — fs.FS cache, DefaultModelsExpandDepth default, content-type sniffing static: - Add sync.Map per-file cache for fs.FS content. Since fs.FS (especially embed.FS) is immutable, cache file bytes on first read. Eliminates repeated heap allocations for the same file — second+ requests for any file are O(1) map lookup + Blob. Particularly important for embed.FS which doesn't implement io.ReadSeeker. - Delete serveFSReadSeeker, serveFSFullRead, serveRangeFromReader (3 funcs). Replace with serveFSCached (1 func). Net: 17 -> 15 functions. - Fix content-type sniffing in servePreCompressedFS: when extension lookup fails, open the original uncompressed file and sniff first 512 bytes via http.DetectContentType. Consistent with OS path behavior. swagger: - DefaultModelsExpandDepth: applyDefaults now treats 0 as unset and defaults to 1 (matching Swagger UI's own default). The basic 3-line setup now produces identical behavior to Swagger UI's defaults. Documented: "0 is treated as unset; use -1 to hide models." * fix: DefaultModelsExpandDepth *int + Options validation - Revert DefaultModelsExpandDepth to *int with IntPtr() helper. nil means "use Swagger UI default (1)". IntPtr(0) correctly sets depth to 0 (show model names only). No more zero-value ambiguity. - Add Options JSON-serializability validation in validate(). Panics at init time if Options contains non-serializable values (functions, channels, etc.) instead of silently falling back to {}. - Tests: TestDefaultModelsExpandDepthZero verifies depth=0 works, TestDefaultModelsExpandDepthNilDefault verifies nil renders as 1, TestOptionsValidationPanic verifies non-serializable Options panic.
docs: add v1.3.2 security notes + update CONTRIBUTING references (#220) SECURITY.md: - Add v1.3.2 section documenting singleflight cross-user data leakage fix, multi-value header replay fix, circuit breaker panic recording, and circuit breaker validation hardening CONTRIBUTING.md: - Update middleware reference implementations by complexity level (simple, stateful, response-transform, request-coalescing) - Add circuitbreaker and singleflight as reference patterns
feat: v1.3.3 utility & serving middleware — pprof, swagger, static, r… …ewrite, adapters (#223) * feat: add pprof, swagger, static, rewrite, and adapters middleware Five new utility & serving middleware for v1.3.3: - pprof (#189): Go profiling endpoints with loopback-only default. Wraps all net/http/pprof handlers via celeris.Adapt(). AuthFunc guard. 10 tests. - swagger (#190): OpenAPI spec + CDN-loaded Swagger UI/Scalar. No bundled assets. SpecContent or fs.FS support. AuthFunc optional. 16 tests. - static (#191): Static file serving with SPA mode, Cache-Control, prefix stripping, directory browse. Path traversal protection via FileFromDir (OS) and path.Clean + .. rejection (fs.FS). 20 tests. - rewrite (#192): Regex-based URL rewriting with capture groups ($1/$2). Pre-routing (Server.Pre). Compile at init, sorted first-match-wins. Optional redirect mode (301/302). 9 tests. - adapters (#193): Bidirectional stdlib ↔ celeris conversion. WrapMiddleware wraps func(http.Handler) http.Handler for celeris chains. ToStdlib delegates to celeris.ToHandler. ReverseProxy via httputil. 8 tests. Documentation: - middleware/doc.go: ordering guide updated (rewrite in Pre, pprof/swagger/static in Use) - README.md: 5 new rows, package count 29 - SECURITY.md: v1.3.3 section (pprof loopback, static traversal, swagger AuthFunc) - CONTRIBUTING.md: new reference implementation categories Closes #189, Closes #190, Closes #191, Closes #192, Closes #193 * fix: resolve all review findings from v1.3.3 audit Security: - swagger: escape SpecURL with html.EscapeString before injecting into HTML/JS templates (prevents XSS via misconfigured SpecURL) Code quality: - pprof: replace manual skipMap with celeris.SkipHelper (per CONTRIBUTING.md) - adapters: fix gofmt formatting + errcheck for resp.Body.Close - .golangci.yml: add pprof var-naming exclusion (package name matches stdlib) * fix: resolve all remaining review findings - swagger/doc.go: add CSP warning for secure middleware interaction (cdn.jsdelivr.net must be allowed in script-src/style-src) - rewrite/doc.go: anchor regex in basic example (^/old$ not /old), add Security section with ReDoS and open redirect warnings - adapters/adapters.go: add 100MB body size cap to responseCapture (prevents OOM from misbehaving stdlib middleware, matches core bridge) * fix: major rework of all 5 middleware to address review findings static: - FIX: Browse XSS — href uses url.PathEscape + ./ prefix (prevents javascript:) - FIX: Prefix matching now segment-boundary-aware (/api no longer matches /api-docs) - ADD: ETag/Last-Modified headers on OS and fs.FS files (conditional GET + 304) - ADD: Range requests (206) for fs.FS files (video seeking, download resume) - ADD: accept-ranges: bytes header - 28 tests covering all new functionality swagger: - FIX: YAML spec auto-detected and served with correct Content-Type - ADD: UIConfig with DocExpansion, DeepLinking, PersistAuthorization, Title - ADD: AssetsPath for air-gapped/self-hosted asset serving (no CDN required) - ADD: RendererScalar/RendererSwaggerUI type for UI engine selection - 26 tests adapters: - FIX: WrapMiddleware now propagates pre-inner headers (rs/cors, gorilla/csrf work) - FIX: buildRequest sets ContentLength, TLS state, Protocol version - ADD: ReverseProxy with Options (WithTransport, WithModifyRequest, WithErrorHandler) - ADD: Pooled responseCapture for zero-alloc hot path - DEL: ToStdlib (was pure alias of celeris.ToHandler) - DOC: Limitations section (no Hijacker/Flusher for WebSocket/streaming) rewrite: - FIX: Rules changed from map[string]string to []Rule slice (insertion-order, not alphabetical — matches user expectations and Echo's approach) - DOC: First-Match-Wins section updated for ordered evaluation * fix: gofmt formatting * fix: security fix + integration review fixes for v1.3.3 middleware Security: - static: fix path traversal in Browse directory listing. serveOS now validates that filepath.Join result stays within root directory (prefix check + symlink escape check), mirroring FileFromDir. Added TestPathTraversalBrowse and TestPathTraversalFile. Core integration: - rewrite: make New() variadic (config ...Config) with defaultConfig and applyDefaults() for pattern consistency with all other middleware - pprof: use cfg := defaultConfig instead of var cfg Config - swagger: use c.Redirect() instead of manual SetHeader + NoContent Tests: - pprof: add TestPprofSkipFunc (was missing Skip callback coverage) - adapters: add TestReverseProxyPanicNilTarget and TestReverseProxyOptions (ReverseProxy had zero test coverage) - rewrite/bench_test.go: fix regex (d+) -> (\d+) so BenchmarkRewriteMatch actually tests the match path Benchmarks: - bench_chain_test.go: add BenchmarkChainPreRoutingWithRewrite, BenchmarkChainRewritePassthrough, BenchmarkChainWithWrapMiddleware - swagger/bench_test.go: fix spec path /swagger/doc.json -> /swagger/spec Documentation: - middleware/doc.go: fix rewrite production stack example ([]Rule syntax), fix swagger description (remove AuthFunc claim), add adapters mention - pprof/swagger/static doc.go: add missing Skip/Ordering sections - adapters doc.go: document ReverseProxy function and Option API - SECURITY.md: fix swagger AuthFunc claim (feature doesn't exist) - README.md: fix static description (no SPA or Cache-Control) * fix: address all 33-agent review findings for v1.3.3 middleware P0 fixes: - adapters: add 100MB body size cap to responseCapture.Write (matches core bridge maxBridgeResponseBytes). Prevents OOM from misbehaving stdlib middleware. - adapters: WrapMiddleware(nil) now panics at init instead of deferred panic on first request. - static: validate() rejects Prefix without leading / (was silently serving nothing). Feature additions: - static: SPA mode (Config.SPA bool) — serves index file for non-existent paths instead of falling through. Works for both OS and fs.FS backends. - static: Cache-Control (Config.MaxAge time.Duration) — sets "public, max-age=N" header alongside ETag/Last-Modified. - swagger: ReDoc renderer (RendererReDoc) with CDN + self-hosted assets support. - swagger: DefaultModelsExpandDepth changed to *int to allow depth=0 (zero-value sentinel collision fixed). - rewrite: per-rule RedirectCode (Rule.RedirectCode) — each rule can override the config-level default (e.g., /old→301, /temp→307). Code quality: - static: precompute filepath.Clean(root) in New() (saves 1 alloc/req) - static: deduplicate ETag computation (setCacheHeaders returns etag, notModified accepts it instead of recomputing) - static: io.ReadAll → make([]byte, size) + io.ReadFull for fs.FS (eliminates O(log N) growth allocations) - static: fix If-None-Match to handle comma-separated ETag lists (RFC 7232 weak comparison with W/ prefix stripping) - static: fix RFC 7232 §6 — skip If-Modified-Since when If-None-Match is present (regardless of match result) - pprof/swagger: use NewHTTPError() for 403/404/405 instead of c.NoContent() (enables global error handler integration) - swagger: HTML-escape Title and AssetsPath in templates - swagger: pin Scalar CDN to @1 major version - rewrite: validate empty Rule.Pattern (panics instead of silently matching everything) Tests (30+ new): - static: SPA mode (OS + FS + disabled), MaxAge, HEAD fs.FS, IMS 304 fs.FS, multi-ETag If-None-Match, Prefix validation panic - swagger: HEAD on /spec, DocExpansion "none", ReDoc renderer + title + assets, DefaultModelsExpandDepth=0 - adapters: WrapMiddleware nil panic, query string round-trip, host propagation, body round-trip, responseCapture body cap, ReverseProxy behavioral (httptest.Server), ReverseProxy error handler - pprof: IPv6 loopback (allow ::1 + deny remote v6), bare prefix, validate Prefix="/" panic - rewrite: per-rule RedirectCode, empty Pattern panic, invalid per-rule RedirectCode panic Benchmarks: - bench_chain_test.go: add BenchmarkChainPprofPassthrough, BenchmarkChainSwaggerPassthrough, BenchmarkChainStaticPassthrough, BenchmarkChainStaticServe (with etag) Documentation: - pprof: fix SkipPaths doc (was "before any other logic", now correctly says "before auth and handler dispatch") - pprof: validate Prefix="/" (would intercept all traffic) - pprof: rename ExampleNew_publicAccess → ExampleNew_tokenAuth, add actual ExampleNew_publicAccess - swagger: doc.go notes UIConfig options are Swagger UI-only - static: doc.go adds SPA Mode + Cache-Control sections - rewrite: Rule.Pattern doc recommends ^ and $ anchors - adapters: doc.go warns about dual CORS + streaming limitation - middleware/doc.go: Pre-routing ordering guidance * fix: goimports ordering + errcheck in lint * feat: address all remaining weakness and feature gaps static: - io.ReadSeeker fast-path: when fs.File implements ReadSeeker, range requests seek to offset and read only the requested bytes instead of loading the entire file into memory. Eliminates 50MB heap allocation for range requests on large files. - Content-type sniffing: when mime.TypeByExtension fails, read first 512 bytes and use http.DetectContentType instead of falling back to application/octet-stream. - Pre-compressed file support (Config.Compress): checks for .br and .gz variants when Accept-Encoding allows, sets Content-Encoding and Vary headers. Brotli preferred over gzip. OS filesystem only. - 7 new tests: ReadSeeker, content-type sniffing (OS+FS+ReadSeeker), pre-compressed (brotli, gzip, not accepted, missing). swagger: - Exported IntPtr helper for DefaultModelsExpandDepth ergonomics. - ReDoc customization: ReDocConfig struct with Theme (dark mode), ExpandResponses, HideDownloadButton, ScrollYOffset, NoAutoAuth. Dark theme maps to full ReDoc color scheme. - OAuth2 UI pre-configuration: OAuth2Config struct with ClientID, ClientSecret, Realm, AppName, Scopes. Generates ui.initOAuth() call in Swagger UI. OAuth2RedirectURL support. - 12 new tests: IntPtr, ReDoc theme/expandResponses/hideDownload/ scrollOffset/noAutoAuth/defaults, OAuth2 full/nil/partial/redirectURL. adapters: - WithModifyResponse option for ReverseProxy: modify response headers from backend before forwarding to client. - http.Flusher support on responseCapture (no-op): prevents panics in stdlib middleware that type-asserts Flusher. Hijacker still unsupported. - Performance section in doc.go: explicitly documents 8-15 alloc cost and recommends native middleware for hot paths. - 3 new tests: ModifyResponse, Flusher type assertion, WrapMiddleware with Flusher. rewrite: - Conditional rewriting: Rule.Methods restricts to specific HTTP methods, Rule.Host restricts to specific Host header value. Methods map built at init time (zero per-request alloc). Both empty means match all. - 4 new tests: method restriction, host restriction, combined, no restriction (backwards compatibility). * fix: gofmt static_test.go * refactor: simplify APIs, fix Flusher footgun, extract static helpers swagger API simplification: - Delete ReDocConfig struct — replaced by generic Options map[string]any that works for ALL renderers (ReDoc, Scalar, Swagger UI). More powerful with fewer types. Users pass raw renderer options as JSON-serializable maps. - Delete IntPtr helper — revert DefaultModelsExpandDepth to plain int. Zero-value (0 = show model names) is a reasonable default. Document clearly. - Add ClientSecret security warning to OAuth2Config doc: values are embedded in page source, only use for dev/test or PKCE public clients. - Net: 3 exported structs (was 4), 0 helper functions (was 1). static refactoring (no behavior changes): - Extract detectContentType() — extension check + http.DetectContentType - Extract sniffContentType() — ReadSeeker-specific with seek-back - Extract serveRangeFromReader() — range handling from io.ReadSeeker - Extract serveFSFullRead() — non-ReadSeeker full-read path - Extract servePreCompressedFS() — pre-compressed file support for fs.FS (was OS-only, now works with embed.FS and custom fs.FS) - serveFS is now a clean orchestrator calling focused helpers - Update Compress doc: works with both OS and fs.FS adapters Flusher fix: - Remove no-op Flush() from responseCapture. A silent no-op is worse than not implementing the interface: SSE middleware calls Flush() expecting data delivery, gets silent success, client receives nothing until response completes. Now the type assertion returns false, so stdlib middleware can take appropriate fallback action. - Remove Unwrap() (returning nil was misleading for ResponseController) - Update doc.go Limitations section * fix: errcheck on rs.Seek in sniffContentType * fix: final polish — fs.FS cache, DefaultModelsExpandDepth default, content-type sniffing static: - Add sync.Map per-file cache for fs.FS content. Since fs.FS (especially embed.FS) is immutable, cache file bytes on first read. Eliminates repeated heap allocations for the same file — second+ requests for any file are O(1) map lookup + Blob. Particularly important for embed.FS which doesn't implement io.ReadSeeker. - Delete serveFSReadSeeker, serveFSFullRead, serveRangeFromReader (3 funcs). Replace with serveFSCached (1 func). Net: 17 -> 15 functions. - Fix content-type sniffing in servePreCompressedFS: when extension lookup fails, open the original uncompressed file and sniff first 512 bytes via http.DetectContentType. Consistent with OS path behavior. swagger: - DefaultModelsExpandDepth: applyDefaults now treats 0 as unset and defaults to 1 (matching Swagger UI's own default). The basic 3-line setup now produces identical behavior to Swagger UI's defaults. Documented: "0 is treated as unset; use -1 to hide models." * fix: DefaultModelsExpandDepth *int + Options validation - Revert DefaultModelsExpandDepth to *int with IntPtr() helper. nil means "use Swagger UI default (1)". IntPtr(0) correctly sets depth to 0 (show model names only). No more zero-value ambiguity. - Add Options JSON-serializability validation in validate(). Panics at init time if Options contains non-serializable values (functions, channels, etc.) instead of silently falling back to {}. - Tests: TestDefaultModelsExpandDepthZero verifies depth=0 works, TestDefaultModelsExpandDepthNilDefault verifies nil renders as 1, TestOptionsValidationPanic verifies non-serializable Options panic.
docs: add v1.3.2 security notes + update CONTRIBUTING references (#220) SECURITY.md: - Add v1.3.2 section documenting singleflight cross-user data leakage fix, multi-value header replay fix, circuit breaker panic recording, and circuit breaker validation hardening CONTRIBUTING.md: - Update middleware reference implementations by complexity level (simple, stateful, response-transform, request-coalescing) - Add circuitbreaker and singleflight as reference patterns
feat: v1.3.3 utility & serving middleware — pprof, swagger, static, r… …ewrite, adapters (#223) * feat: add pprof, swagger, static, rewrite, and adapters middleware Five new utility & serving middleware for v1.3.3: - pprof (#189): Go profiling endpoints with loopback-only default. Wraps all net/http/pprof handlers via celeris.Adapt(). AuthFunc guard. 10 tests. - swagger (#190): OpenAPI spec + CDN-loaded Swagger UI/Scalar. No bundled assets. SpecContent or fs.FS support. AuthFunc optional. 16 tests. - static (#191): Static file serving with SPA mode, Cache-Control, prefix stripping, directory browse. Path traversal protection via FileFromDir (OS) and path.Clean + .. rejection (fs.FS). 20 tests. - rewrite (#192): Regex-based URL rewriting with capture groups ($1/$2). Pre-routing (Server.Pre). Compile at init, sorted first-match-wins. Optional redirect mode (301/302). 9 tests. - adapters (#193): Bidirectional stdlib ↔ celeris conversion. WrapMiddleware wraps func(http.Handler) http.Handler for celeris chains. ToStdlib delegates to celeris.ToHandler. ReverseProxy via httputil. 8 tests. Documentation: - middleware/doc.go: ordering guide updated (rewrite in Pre, pprof/swagger/static in Use) - README.md: 5 new rows, package count 29 - SECURITY.md: v1.3.3 section (pprof loopback, static traversal, swagger AuthFunc) - CONTRIBUTING.md: new reference implementation categories Closes #189, Closes #190, Closes #191, Closes #192, Closes #193 * fix: resolve all review findings from v1.3.3 audit Security: - swagger: escape SpecURL with html.EscapeString before injecting into HTML/JS templates (prevents XSS via misconfigured SpecURL) Code quality: - pprof: replace manual skipMap with celeris.SkipHelper (per CONTRIBUTING.md) - adapters: fix gofmt formatting + errcheck for resp.Body.Close - .golangci.yml: add pprof var-naming exclusion (package name matches stdlib) * fix: resolve all remaining review findings - swagger/doc.go: add CSP warning for secure middleware interaction (cdn.jsdelivr.net must be allowed in script-src/style-src) - rewrite/doc.go: anchor regex in basic example (^/old$ not /old), add Security section with ReDoS and open redirect warnings - adapters/adapters.go: add 100MB body size cap to responseCapture (prevents OOM from misbehaving stdlib middleware, matches core bridge) * fix: major rework of all 5 middleware to address review findings static: - FIX: Browse XSS — href uses url.PathEscape + ./ prefix (prevents javascript:) - FIX: Prefix matching now segment-boundary-aware (/api no longer matches /api-docs) - ADD: ETag/Last-Modified headers on OS and fs.FS files (conditional GET + 304) - ADD: Range requests (206) for fs.FS files (video seeking, download resume) - ADD: accept-ranges: bytes header - 28 tests covering all new functionality swagger: - FIX: YAML spec auto-detected and served with correct Content-Type - ADD: UIConfig with DocExpansion, DeepLinking, PersistAuthorization, Title - ADD: AssetsPath for air-gapped/self-hosted asset serving (no CDN required) - ADD: RendererScalar/RendererSwaggerUI type for UI engine selection - 26 tests adapters: - FIX: WrapMiddleware now propagates pre-inner headers (rs/cors, gorilla/csrf work) - FIX: buildRequest sets ContentLength, TLS state, Protocol version - ADD: ReverseProxy with Options (WithTransport, WithModifyRequest, WithErrorHandler) - ADD: Pooled responseCapture for zero-alloc hot path - DEL: ToStdlib (was pure alias of celeris.ToHandler) - DOC: Limitations section (no Hijacker/Flusher for WebSocket/streaming) rewrite: - FIX: Rules changed from map[string]string to []Rule slice (insertion-order, not alphabetical — matches user expectations and Echo's approach) - DOC: First-Match-Wins section updated for ordered evaluation * fix: gofmt formatting * fix: security fix + integration review fixes for v1.3.3 middleware Security: - static: fix path traversal in Browse directory listing. serveOS now validates that filepath.Join result stays within root directory (prefix check + symlink escape check), mirroring FileFromDir. Added TestPathTraversalBrowse and TestPathTraversalFile. Core integration: - rewrite: make New() variadic (config ...Config) with defaultConfig and applyDefaults() for pattern consistency with all other middleware - pprof: use cfg := defaultConfig instead of var cfg Config - swagger: use c.Redirect() instead of manual SetHeader + NoContent Tests: - pprof: add TestPprofSkipFunc (was missing Skip callback coverage) - adapters: add TestReverseProxyPanicNilTarget and TestReverseProxyOptions (ReverseProxy had zero test coverage) - rewrite/bench_test.go: fix regex (d+) -> (\d+) so BenchmarkRewriteMatch actually tests the match path Benchmarks: - bench_chain_test.go: add BenchmarkChainPreRoutingWithRewrite, BenchmarkChainRewritePassthrough, BenchmarkChainWithWrapMiddleware - swagger/bench_test.go: fix spec path /swagger/doc.json -> /swagger/spec Documentation: - middleware/doc.go: fix rewrite production stack example ([]Rule syntax), fix swagger description (remove AuthFunc claim), add adapters mention - pprof/swagger/static doc.go: add missing Skip/Ordering sections - adapters doc.go: document ReverseProxy function and Option API - SECURITY.md: fix swagger AuthFunc claim (feature doesn't exist) - README.md: fix static description (no SPA or Cache-Control) * fix: address all 33-agent review findings for v1.3.3 middleware P0 fixes: - adapters: add 100MB body size cap to responseCapture.Write (matches core bridge maxBridgeResponseBytes). Prevents OOM from misbehaving stdlib middleware. - adapters: WrapMiddleware(nil) now panics at init instead of deferred panic on first request. - static: validate() rejects Prefix without leading / (was silently serving nothing). Feature additions: - static: SPA mode (Config.SPA bool) — serves index file for non-existent paths instead of falling through. Works for both OS and fs.FS backends. - static: Cache-Control (Config.MaxAge time.Duration) — sets "public, max-age=N" header alongside ETag/Last-Modified. - swagger: ReDoc renderer (RendererReDoc) with CDN + self-hosted assets support. - swagger: DefaultModelsExpandDepth changed to *int to allow depth=0 (zero-value sentinel collision fixed). - rewrite: per-rule RedirectCode (Rule.RedirectCode) — each rule can override the config-level default (e.g., /old→301, /temp→307). Code quality: - static: precompute filepath.Clean(root) in New() (saves 1 alloc/req) - static: deduplicate ETag computation (setCacheHeaders returns etag, notModified accepts it instead of recomputing) - static: io.ReadAll → make([]byte, size) + io.ReadFull for fs.FS (eliminates O(log N) growth allocations) - static: fix If-None-Match to handle comma-separated ETag lists (RFC 7232 weak comparison with W/ prefix stripping) - static: fix RFC 7232 §6 — skip If-Modified-Since when If-None-Match is present (regardless of match result) - pprof/swagger: use NewHTTPError() for 403/404/405 instead of c.NoContent() (enables global error handler integration) - swagger: HTML-escape Title and AssetsPath in templates - swagger: pin Scalar CDN to @1 major version - rewrite: validate empty Rule.Pattern (panics instead of silently matching everything) Tests (30+ new): - static: SPA mode (OS + FS + disabled), MaxAge, HEAD fs.FS, IMS 304 fs.FS, multi-ETag If-None-Match, Prefix validation panic - swagger: HEAD on /spec, DocExpansion "none", ReDoc renderer + title + assets, DefaultModelsExpandDepth=0 - adapters: WrapMiddleware nil panic, query string round-trip, host propagation, body round-trip, responseCapture body cap, ReverseProxy behavioral (httptest.Server), ReverseProxy error handler - pprof: IPv6 loopback (allow ::1 + deny remote v6), bare prefix, validate Prefix="/" panic - rewrite: per-rule RedirectCode, empty Pattern panic, invalid per-rule RedirectCode panic Benchmarks: - bench_chain_test.go: add BenchmarkChainPprofPassthrough, BenchmarkChainSwaggerPassthrough, BenchmarkChainStaticPassthrough, BenchmarkChainStaticServe (with etag) Documentation: - pprof: fix SkipPaths doc (was "before any other logic", now correctly says "before auth and handler dispatch") - pprof: validate Prefix="/" (would intercept all traffic) - pprof: rename ExampleNew_publicAccess → ExampleNew_tokenAuth, add actual ExampleNew_publicAccess - swagger: doc.go notes UIConfig options are Swagger UI-only - static: doc.go adds SPA Mode + Cache-Control sections - rewrite: Rule.Pattern doc recommends ^ and $ anchors - adapters: doc.go warns about dual CORS + streaming limitation - middleware/doc.go: Pre-routing ordering guidance * fix: goimports ordering + errcheck in lint * feat: address all remaining weakness and feature gaps static: - io.ReadSeeker fast-path: when fs.File implements ReadSeeker, range requests seek to offset and read only the requested bytes instead of loading the entire file into memory. Eliminates 50MB heap allocation for range requests on large files. - Content-type sniffing: when mime.TypeByExtension fails, read first 512 bytes and use http.DetectContentType instead of falling back to application/octet-stream. - Pre-compressed file support (Config.Compress): checks for .br and .gz variants when Accept-Encoding allows, sets Content-Encoding and Vary headers. Brotli preferred over gzip. OS filesystem only. - 7 new tests: ReadSeeker, content-type sniffing (OS+FS+ReadSeeker), pre-compressed (brotli, gzip, not accepted, missing). swagger: - Exported IntPtr helper for DefaultModelsExpandDepth ergonomics. - ReDoc customization: ReDocConfig struct with Theme (dark mode), ExpandResponses, HideDownloadButton, ScrollYOffset, NoAutoAuth. Dark theme maps to full ReDoc color scheme. - OAuth2 UI pre-configuration: OAuth2Config struct with ClientID, ClientSecret, Realm, AppName, Scopes. Generates ui.initOAuth() call in Swagger UI. OAuth2RedirectURL support. - 12 new tests: IntPtr, ReDoc theme/expandResponses/hideDownload/ scrollOffset/noAutoAuth/defaults, OAuth2 full/nil/partial/redirectURL. adapters: - WithModifyResponse option for ReverseProxy: modify response headers from backend before forwarding to client. - http.Flusher support on responseCapture (no-op): prevents panics in stdlib middleware that type-asserts Flusher. Hijacker still unsupported. - Performance section in doc.go: explicitly documents 8-15 alloc cost and recommends native middleware for hot paths. - 3 new tests: ModifyResponse, Flusher type assertion, WrapMiddleware with Flusher. rewrite: - Conditional rewriting: Rule.Methods restricts to specific HTTP methods, Rule.Host restricts to specific Host header value. Methods map built at init time (zero per-request alloc). Both empty means match all. - 4 new tests: method restriction, host restriction, combined, no restriction (backwards compatibility). * fix: gofmt static_test.go * refactor: simplify APIs, fix Flusher footgun, extract static helpers swagger API simplification: - Delete ReDocConfig struct — replaced by generic Options map[string]any that works for ALL renderers (ReDoc, Scalar, Swagger UI). More powerful with fewer types. Users pass raw renderer options as JSON-serializable maps. - Delete IntPtr helper — revert DefaultModelsExpandDepth to plain int. Zero-value (0 = show model names) is a reasonable default. Document clearly. - Add ClientSecret security warning to OAuth2Config doc: values are embedded in page source, only use for dev/test or PKCE public clients. - Net: 3 exported structs (was 4), 0 helper functions (was 1). static refactoring (no behavior changes): - Extract detectContentType() — extension check + http.DetectContentType - Extract sniffContentType() — ReadSeeker-specific with seek-back - Extract serveRangeFromReader() — range handling from io.ReadSeeker - Extract serveFSFullRead() — non-ReadSeeker full-read path - Extract servePreCompressedFS() — pre-compressed file support for fs.FS (was OS-only, now works with embed.FS and custom fs.FS) - serveFS is now a clean orchestrator calling focused helpers - Update Compress doc: works with both OS and fs.FS adapters Flusher fix: - Remove no-op Flush() from responseCapture. A silent no-op is worse than not implementing the interface: SSE middleware calls Flush() expecting data delivery, gets silent success, client receives nothing until response completes. Now the type assertion returns false, so stdlib middleware can take appropriate fallback action. - Remove Unwrap() (returning nil was misleading for ResponseController) - Update doc.go Limitations section * fix: errcheck on rs.Seek in sniffContentType * fix: final polish — fs.FS cache, DefaultModelsExpandDepth default, content-type sniffing static: - Add sync.Map per-file cache for fs.FS content. Since fs.FS (especially embed.FS) is immutable, cache file bytes on first read. Eliminates repeated heap allocations for the same file — second+ requests for any file are O(1) map lookup + Blob. Particularly important for embed.FS which doesn't implement io.ReadSeeker. - Delete serveFSReadSeeker, serveFSFullRead, serveRangeFromReader (3 funcs). Replace with serveFSCached (1 func). Net: 17 -> 15 functions. - Fix content-type sniffing in servePreCompressedFS: when extension lookup fails, open the original uncompressed file and sniff first 512 bytes via http.DetectContentType. Consistent with OS path behavior. swagger: - DefaultModelsExpandDepth: applyDefaults now treats 0 as unset and defaults to 1 (matching Swagger UI's own default). The basic 3-line setup now produces identical behavior to Swagger UI's defaults. Documented: "0 is treated as unset; use -1 to hide models." * fix: DefaultModelsExpandDepth *int + Options validation - Revert DefaultModelsExpandDepth to *int with IntPtr() helper. nil means "use Swagger UI default (1)". IntPtr(0) correctly sets depth to 0 (show model names only). No more zero-value ambiguity. - Add Options JSON-serializability validation in validate(). Panics at init time if Options contains non-serializable values (functions, channels, etc.) instead of silently falling back to {}. - Tests: TestDefaultModelsExpandDepthZero verifies depth=0 works, TestDefaultModelsExpandDepthNilDefault verifies nil renders as 1, TestOptionsValidationPanic verifies non-serializable Options panic.
docs: add v1.3.2 security notes + update CONTRIBUTING references (#220) SECURITY.md: - Add v1.3.2 section documenting singleflight cross-user data leakage fix, multi-value header replay fix, circuit breaker panic recording, and circuit breaker validation hardening CONTRIBUTING.md: - Update middleware reference implementations by complexity level (simple, stateful, response-transform, request-coalescing) - Add circuitbreaker and singleflight as reference patterns
PreviousNext