Skip to content

Tags: goceleris/celeris

Tags

v1.3.4

Toggle v1.3.4's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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.

middleware/otel/v1.3.4

Toggle middleware/otel/v1.3.4's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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.

middleware/metrics/v1.3.4

Toggle middleware/metrics/v1.3.4's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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.

middleware/compress/v1.3.4

Toggle middleware/compress/v1.3.4's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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.

v1.3.3

Toggle v1.3.3's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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.

v1.3.2

Toggle v1.3.2's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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

middleware/otel/v1.3.3

Toggle middleware/otel/v1.3.3's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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.

middleware/otel/v1.3.2

Toggle middleware/otel/v1.3.2's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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

middleware/metrics/v1.3.3

Toggle middleware/metrics/v1.3.3's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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.

middleware/metrics/v1.3.2

Toggle middleware/metrics/v1.3.2's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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