🐛 bug: fix correctness issues across core, router, binder, client and middleware - #4652
Conversation
…th and app lifecycle issues Cache middleware: - retire the superseded heap node when a no-cache/Pragma request refreshes an entry, so the stale node no longer evicts the live entry and double-counts bytes - run eviction only once the response is known to be stored, so uncacheable responses no longer displace valid entries - treat a missing "_body" entry on external storage as a miss instead of a 500 - never store 206 Partial Content (RFC 9111 §3.3) - keep CacheInvalidator from mutating the shared in-memory item (data race) - clamp the resident-time computation when the clock steps backwards Other middleware and storage: - limiter: keep sub-second Expiration values instead of falling back to 60s - internal memory storages: round TTLs up to whole seconds, treat negative values as "no expiry", and honor sub-second GCInterval - session: re-arm the absolute expiration after Reset - etag: answer 304 Not Modified only for GET and HEAD (RFC 9110 §13.1.2) - keyauth: add the WWW-Authenticate challenge when a custom ErrorHandler returns a 401/407 *fiber.Error App lifecycle: - graceful shutdown no longer runs Shutdown (and the shutdown hooks) a second time when Listen returns, nor for a Listen that failed before serving - ShutdownWithContext releases the app mutex while draining, so handlers that take it (RebuildTree, RemoveRoute, ...) cannot deadlock the shutdown - ListenConfig.ShutdownTimeout defaults to 10s with a user config too; a negative value disables it - Listen refuses to serve plaintext when only one of CertFile/CertKeyFile is set - TrustProxyConfig.Proxies are stored in canonical form so any IPv6 spelling matches the peer address - TLSHandler records ClientHelloInfo per connection instead of app-wide - services must have unique names and are terminated in reverse start order Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NcsBE6AGr9Ey8cpaiELUs8
…d binder splitting issues Request/response (root package): - SendFile no longer panics when the configured fs.FS is an uncomparable type (fstest.MapFS, structs holding maps or slices): file systems are compared by identity instead of with == - SendFile leaves the request's Accept-Encoding header alone, so the compress middleware and later handlers still see what the client accepts - AutoFormat answers text/plain when the Accept header is absent or nothing matches, as documented, instead of text/html - content negotiation takes an offer's weight from its most specific matching range (RFC 9110 §12.5.1), so "text/*;q=1, text/html;q=0.5" prefers text/plain; the common uniform-weight and browser headers stay on the cheap first-match path - IPv4-mapped IPv6 addresses (::ffff:a.b.c.d) in X-Forwarded-For are accepted by IP validation and recognized as trusted proxies - Format handlers and custom binders receive the custom context when the app uses one, so c.(*MyCtx) assertions hold in them - flash messages are read for requests built programmatically (adaptor, app.Handler on a hand-made RequestCtx), which carry no raw headers - the identity step of a multi-coding Content-Encoding chain no longer re-sets the body with a slice aliasing it Binder: - comma splitting resolves the key to the field it names (dot, bracket and mixed notation, any depth, slices of structs, embedded structs), so a scalar field beside a slice is no longer split and nested slices are - only map[string][]string destinations split; map[string]string keeps values whole - non-pointer or nil destinations no longer panic before the decoder reports them - a tag with options but no alias falls back to the Go field name, like schema - docs: bound values alias request memory (Immutable does not apply) and ParserConfig.SetAliasTag is documented as ignored Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NcsBE6AGr9Ey8cpaiELUs8
…nts, strict slashes and path unescaping - constraints are parsed from the pattern as written, so case-insensitive routing no longer lowercases regex classes, datetime layouts or custom constraint names (and no longer drops mixed-case custom constraints) - Method() carries the routing position over to the new method's tree, so a middleware overriding the method no longer skips endpoints - automatic HEAD routes are registered for sub-apps mounted after their parent (nested mounts discovered at startup) and for routes published with RebuildTree - Use with a prefix slice mounts the sub-app under every prefix - an escaped "/\*" pattern is a literal path rather than the catch-all route - StrictRouting applies to parametric routes ending in "/" as it does to static ones - UnescapePath decodes %XX only: a "+" stays a "+" (RFC 3986) and malformed escapes are kept as they are - Name applies to every route registered by a multi-method Add/All call - mounting an app onto itself panics instead of deadlocking at startup Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NcsBE6AGr9Ey8cpaiELUs8
- timeout: the default path returns fiber.ErrRequestTimeout as documented, and a *fiber.Error returned by OnTimeout shapes the frozen response - proxy: DomainForward passes non-matching hosts to the next handler and matches a Host carrying a port; the forwarding helpers restore the client's Host header after the upstream call - static: the attachment header is only set on served files (no leak onto the next handler's response) and keeps the detected Content-Type; a handler registered on several routes serves each under its own prefix; MaxAge is applied to successful responses only - compress: Accept-Encoding is negotiated with weights, wildcard and unspaced lists honored (server preference br > zstd > gzip > deflate on ties), the client's header is left untouched, and a streamed body keeps a weak ETag instead of being drained to recompute a strong one - sse: sub-millisecond retry delays round up to 1 ms instead of "retry: 0" - docs: logger byte tags describe the values actually logged; compress HEAD handling matches the code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NcsBE6AGr9Ey8cpaiELUs8
- adaptor: CopyContextToFiberContext walks the whole derived-context chain (WithTimeout, WithDeadline, WithoutCancel, AfterFunc), requests net/http served over TLS are seen as TLS by Fiber, HTTPMiddleware routes a rewritten r.URL (http.StripPrefix) and no longer races with a middleware that flushes before calling next - a path override carries the routing position over to the new tree bucket, so Next resumes after the current route instead of at a stale offset - retry: every Retry call starts at InitialInterval and keeps its state local, so a shared ExponentialBackoff is safe for concurrent use - log: Fatal terminates the program even when the level suppresses its output - paginate: an empty SortKey no longer reads a nameless "?=..." parameter; NextPageURL does not overflow at the maximum page - favicon: MaxBytes at the top of the range no longer overflows the read limit - docs: adaptor example reads copied values with c.Locals; default logger writes to stderr Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NcsBE6AGr9Ey8cpaiELUs8
…Max-Age and stream handling - Accept an upper-case scheme when deciding whether to prepend the base URL - Clear each merged header key before applying it, so resending a request does not accumulate values and request headers override client headers - Keep a cookie's name and value when fasthttp rejects one of its attributes, instead of dropping every cookie of the response - Copy the request and read its settings before the execution goroutine starts, so an already-cancelled context cannot race with a release - Release only the response (and a client-created request) when a response hook fails, leaving caller-acquired requests to the caller - Track client-created requests so Response.Close releases those alone - Sort header, cookie and parameter keys stably - Return a copy from Response.Header so it survives releasing the response - Honor Max-Age over Expires in the cookie jar, expiring on zero or less - Log only headers for streamed bodies and skip logging without a logger - Update the client docs for the changed behaviours Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NcsBE6AGr9Ey8cpaiELUs8
…efork shutdown - Skip the app's ErrorHandler for a request whose response fasthttp already holds as a timeout response: anything written then is ignored, and the handler that timed out may still be writing to the same context, which the race detector caught once the default timeout path returned its error - Regenerate the Ctx interface for ctxForHandlers - Document that GracefulContext and Shutdown act on one prefork process - Restore the routeParser comment on Route Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NcsBE6AGr9Ey8cpaiELUs8
Send picks at random between a ready response and a canceled context, so an instantly answering server could win the race the test asserts on. The handler now takes long enough that the cancellation is the only ready case. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NcsBE6AGr9Ey8cpaiELUs8
…ps its server A canceled send returns before anything is dialed, so under a starved scheduler the test could shut the server down before Serve had registered the listener, which made the shutdown a no-op and the stop time out. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NcsBE6AGr9Ey8cpaiELUs8
Test comments are at most one line and only where they add something; source comments are cut down to what the reader needs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NcsBE6AGr9Ey8cpaiELUs8
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NcsBE6AGr9Ey8cpaiELUs8
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
Security findingsAdvisory findings (1)ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (1)
Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review. WalkthroughThis change set updates retry state, routing, request handling, middleware behavior, cache accounting, storage expiration, service shutdown, TLS handling, logging, and related documentation and tests. ChangesRetry state
Routing and request context
Client and adaptor flow
Middleware behavior
Cache, storage, and lifecycle
Listener and logging lifecycle
Documentation
Estimated code review effort: 5 (Critical) | ~120 minutes Merge Risk: 🟡 Moderate · up to The change updates broad Fiber lifecycle and request behavior, but unresolved conditional-write, routing, mount-cycle, and client-request ownership concerns could cause incorrect request handling or availability problems in affected applications. These issues should be resolved or explicitly accepted before merge. Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4652 +/- ##
==========================================
+ Coverage 93.80% 94.18% +0.38%
==========================================
Files 139 139
Lines 16229 16881 +652
==========================================
+ Hits 15223 15899 +676
+ Misses 636 627 -9
+ Partials 370 355 -15
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
🟡 Changes recommended
State.deleteService currently compares Service interface values directly (started == srv), which can panic for uncomparable service implementations and should be replaced with a safe identifier comparison.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR applies a broad correctness audit across Fiber core (app lifecycle, routing, ctx/request/response), the REST client, binders, and multiple middlewares, pairing behavior fixes with regression tests and documentation updates so the corrected semantics are pinned going forward.
Changes:
- Fixes service lifecycle correctness (duplicate-name validation, deterministic reverse shutdown order) and multiple router correctness issues (constraints preservation under case-insensitive routing, StrictRouting edge cases, method/path override continuity, auto-HEAD updates).
- Hardens request/response behaviors (safe
SendFileFS comparison, preservesAccept-Encoding, correctAutoFormatfallback, path unescape semantics, proxy/IP validation, timeout/error-handler interactions). - Updates client and middleware correctness (cookie parsing tolerance, request ownership on
Response.Close, header merge semantics, compression negotiation, proxy forwarding host restoration) with extensive new tests and docs.
File summaries
| File | Description |
|---|---|
| state.go | Track started services in-order with a mutex for deterministic shutdown. |
| services.go | Validate services slice upfront; shutdown in reverse start order. |
| services_test.go | Adds tests for reverse shutdown order and duplicate service names. |
| router.go | Route IDs for cross-tree Next() continuity; constraints adoption; auto-HEAD behavior updates. |
| res.go | Safer FS equality for SendFile; custom ctx propagation for Format; AutoFormat fallback; preserve Accept-Encoding. |
| req.go | Fix identity coding body aliasing; IPv4-mapped IPv6 validation; method/path override index carry-over; trusted proxy unmap. |
| redirect.go | Flash-cookie detection works for programmatic requests with empty raw headers. |
| redirect_test.go | Regression test for programmatic flash cookie parsing/clearing. |
| path.go | Correct path unescaping (+ literal, malformed escapes preserved); strict routing for param routes; constraint adoption. |
| mount.go | Prevent self-mount deadlock; collect nested sub-apps before startup processing. |
| middleware/timeout/timeout.go | Ensure timeout errors propagate; allow *fiber.Error from OnTimeout to shape response. |
| middleware/timeout/timeout_test.go | Tests for timeout error propagation, shaping, and skipping app ErrorHandler. |
| middleware/timeout/config.go | Clarifies OnTimeout semantics and concurrency expectations. |
| middleware/static/static.go | Per-route file handlers; avoid attachment leakage on misses; preserve detected Content-Type; MaxAge only on success. |
| middleware/static/static_test.go | Tests for download/header leakage, per-prefix correctness, and MaxAge on errors. |
| middleware/sse/sse.go | Round sub-millisecond retry up to 1ms. |
| middleware/sse/sse_test.go | Regression test for sub-millisecond retry rounding. |
| middleware/sse/event.go | Shared retry-ms rounding helper for event output. |
| middleware/session/session.go | Preserve/restore absolute expiration across session reset. |
| middleware/session/session_test.go | Regression test that rotated sessions still expire absolutely. |
| middleware/proxy/proxy.go | Restore original Host after forwarding; DomainForward continues on mismatch; match Host with optional port. |
| middleware/proxy/proxy_test.go | Tests for DomainForward mismatch continuation and Forward host restoration. |
| middleware/paginate/paginate.go | Ignore sort query when SortKey is empty to prevent nameless param control. |
| middleware/paginate/paginate_test.go | Tests for empty SortKey behavior and NextPageURL overflow handling. |
| middleware/paginate/page_info.go | Prevent integer overflow in next-page calculation. |
| middleware/limiter/limiter_test.go | Regression tests for sub-second expiration semantics and Retry-After behavior. |
| middleware/limiter/config.go | Treat any non-positive Expiration as default (preserve sub-second positives). |
| middleware/keyauth/keyauth.go | Add challenge when ErrorHandler returns *fiber.Error with 401/407. |
| middleware/keyauth/keyauth_test.go | Regression test for challenge behavior when ErrorHandler returns errors. |
| middleware/favicon/favicon.go | Avoid MaxBytes+1 overflow for math.MaxInt64. |
| middleware/favicon/favicon_test.go | Regression test for MaxBytes overflow behavior. |
| middleware/etag/etag.go | Restrict 304 behavior to GET/HEAD while still setting ETag for others. |
| middleware/etag/etag_test.go | Regression tests for method restrictions around If-None-Match/304. |
| middleware/compress/compress.go | Proper Accept-Encoding negotiation; avoid draining streams for ETag; restore request headers post-negotiation. |
| middleware/compress/compress_test.go | Tests for negotiation, weak ETag on streams, and request header preservation. |
| middleware/cache/utils.go | Prevent resident-time underflow when clock steps backwards. |
| middleware/cache/config.go | Adds internal accounting hook field (used by cache internals/tests). |
| middleware/adaptor/adaptor.go | Copy context values through derived contexts; propagate TLS state; respect URL rewrites; guard against post-flush next. |
| middleware/adaptor/adaptor_test.go | Tests for derived-context copying, TLS propagation, and URL rewrite routing. |
| log/default.go | Ensure Fatal exits even when log level suppresses output; add test hook via osExit. |
| log/default_test.go | Regression test for fatal exit behavior above log level. |
| listen.go | Default ShutdownTimeout applied; negative disables timeout; reject partial TLS keypair config; graceful shutdown goroutine stop signal. |
| listen_test.go | Tests for shutdown hooks once, no hooks on listen error, timeout defaults/negative semantics, partial TLS config failure, per-conn CHI. |
| internal/storage/memory/memory.go | Round expiration up to whole seconds; clamp; clarify non-positive semantics. |
| internal/storage/memory/memory_test.go | Tests for rounding-up expirations and sub-second GC intervals. |
| internal/storage/memory/config.go | Treat non-positive GCInterval as default (preserve sub-second positives). |
| internal/memory/memory.go | Round TTL up to whole seconds with clamp. |
| internal/memory/memory_test.go | Tests for TTL rounding and sub-second expiry. |
| hooks_test.go | Make tests that redirect global logger output non-parallel; restore output with cleanup. |
| helpers.go | RFC 9110 specificity-aware content negotiation resolution; new helper functions for offer selection. |
| docs/middleware/timeout.md | Documents timeout limitations: handler goroutine behavior and ErrorHandler skip semantics. |
| docs/middleware/static.md | Documents per-route prefix handling, Download header behavior, and MaxAge only on success. |
| docs/middleware/sse.md | Documents retry ms rounding behavior. |
| docs/middleware/proxy.md | Documents host matching with/without port and restoration after forwarding. |
| docs/middleware/logger.md | Corrects bytesSent/bytesReceived semantics timing and sentinel values. |
| docs/middleware/keyauth.md | Documents challenge behavior when ErrorHandler returns *fiber.Error. |
| docs/middleware/etag.md | Documents 304 limited to GET/HEAD; other methods pass through. |
| docs/middleware/compress.md | Documents encoding negotiation, header preservation, and weak ETag for streamed bodies. |
| docs/middleware/cache.md | Documents that 206 is not stored and why (range semantics). |
| docs/middleware/adaptor.md | Documents Locals vs Context value access, derived-context copying, TLS propagation, and rewrite behavior/limits. |
| docs/guide/routing.md | Documents escaped * literal behavior, constraint case handling, and auto-HEAD behavior with rebuild/mount nesting. |
| docs/client/rest.md | Documents helper-owned request lifecycle; scheme case-insensitivity; redirect constraints. |
| docs/client/response.md | Documents Header returns a copy; Close request-ownership semantics. |
| docs/client/request.md | Documents caller-owned requests; header override semantics; JSON/XML header behavior; redirect constraints. |
| docs/client/hooks.md | Documents header override semantics; tolerant cookie parsing; debug logging behavior with streams/nil logger. |
| docs/api/services.md | Documents uniqueness requirement and reverse shutdown order. |
| docs/api/log.md | Corrects default logger output stream description. |
| docs/api/fiber.md | Documents StrictRouting param routes; UnescapePath semantics; TLS config requirement; graceful shutdown semantics (incl. prefork). |
| docs/api/ctx.md | Documents Accept specificity rule; per-connection ClientHelloInfo; IP validation accepts IPv4-mapped IPv6; SendFile Accept-Encoding behavior. |
| docs/api/bind.md | Clarifies bound string lifetime; Immutable does not apply; ParserConfig SetAliasTag ignored. |
| docs/api/app.md | Documents multi-prefix mount behavior, self-mount panic, Name applies to multi-method registration, ErrorHandler skip after fasthttp timeout response, auto-HEAD on RebuildTree. |
| ctx.go | Per-connection ClientHelloInfo tracking; handler ctx helper; unescape path semantics; bind uses handler ctx. |
| ctx_test.go | Adds regression tests for SendFile FS comparability, Accept-Encoding preservation, AutoFormat fallback, IP validation, Accept specificity, custom ctx in Format, identity chain. |
| ctx_interface_gen.go | Updates interface docs and adds ctxForHandlers to generated interface. |
| client/response.go | Header returns copied string; Close releases request only if client-owned. |
| client/request.go | Stable sort for params/form data; reset clears client/clientOwned. |
| client/hooks.go | Case-insensitive scheme detection; clear/override header merge; tolerant cookie parsing; debug logging avoids draining streams and handles nil logger. |
| client/core.go | Capture request state before goroutine to avoid races on cancel; correct ownership cleanup on hook errors; redirect-method constraints. |
| client/cookiejar.go | Honor Max-Age precedence (incl. Max-Age=0/negative) with attribute-presence detection. |
| client/client.go | REST helpers use client-owned requests that Response.Close releases. |
| client/client_test.go | Regression tests for cookie parsing tolerance, request ownership, debug stream logging, Max-Age, cancellation race, header override, stable param order, Header copying, resend headers, uppercase scheme. |
| binder/query_test.go | Adds tests for correct splitting resolution across nested/embedded structures and invalid destinations. |
| binder/mapping.go | Reworks key-to-field resolution to avoid incorrect splitting; adds promoted/embedded handling and depth bounds. |
| binder/mapping_test.go | Expands tests for new splitting/key-resolution behavior and fieldName parsing. |
| binder/header_test.go | Tests map[string]string not being split while map[string][]string is. |
| bind_test.go | Regression test ensuring custom ctx reaches custom binder Parse and Body bind. |
| app.go | Canonicalize trusted proxy IP storage; Name applies to multi-method registrations; mount all prefixes; avoid ShutdownWithContext mutex deadlock; skip ErrorHandler after committed timeout response; hook ConnState for TLS handler cleanup; collect sub-apps before auto-HEAD. |
| app_test.go | Tests for shutdown mutex deadlock fix, canonical trusted proxies (incl. v4-mapped), ErrorHandler skip on committed timeout response. |
| addon/retry/README.md | Documents per-call backoff state semantics. |
| addon/retry/exponential_backoff.go | Make Retry state per-call (no shared mutation); clarify currentInterval semantics. |
| addon/retry/exponential_backoff_test.go | Tests for per-call reset and concurrency safety. |
Review details
Files not reviewed (1)
- ctx_interface_gen.go: Generated file
- Files reviewed: 93/94 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…test The test wrote its file into t.TempDir(), which cannot be removed on Windows while the static handler still holds it open: fasthttp's FS keeps a file open for FSHandlerCacheDuration (10s), far longer than the cleanup retries, so the job failed on both Windows runners. Serve .github/testdata/fs/img/fiberpng instead — a PNG with no extension, so only content detection can type it, and no directory needs cleaning up. The test still fails without the fix, with an empty Content-Type. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NcsBE6AGr9Ey8cpaiELUs8
This comment has been minimized.
This comment has been minimized.
…er credit test deterministic deleteService compared Service interface values with ==, which panics when a service's dynamic type is uncomparable (one holding a slice or map, stored by value). Match on the name instead, which startServices already validates as unique and which the State map is keyed by. Test_Limiter_Fixed_Window_SkipSuccessfulRequests_DoesNotCreditNextWindow drove a 300ms expiration with wall-clock sleeps. Windows are whole seconds, so that expiration was silently replaced by the one-minute default and the rollover the test describes never happened; now that a sub-second expiration is honored as a one-second window, whether it rolls over depends on where the run falls inside the current second. Drive it with the injected clock the other window tests use, so the rollover happens exactly once and where the test says it does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NcsBE6AGr9Ey8cpaiELUs8
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 15b0e60097
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…hen nothing changed App.init already starts configured services, so calling startServices again registered each of them a second time in the start-order slice and shutdown terminated them twice. setService now replaces the entry a name already holds, matching the State map it writes alongside; the shutdown benchmark drops back from 122ms to 61ms because each service is terminated once again. RebuildTree rebuilt the whole auto-HEAD lookup on every call. Skip it while no route has been registered since the last pass and the HEAD stack is unchanged: 71.8us/42563 B/op to 49.4us/33050 B/op. The rest of the difference from main is the companions themselves, which the tree is now expected to carry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NcsBE6AGr9Ey8cpaiELUs8
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7cd34a9266
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The package-level osExit this branch added is global mutable state, so the two tests that intercept it had to skip t.Parallel(), which AGENTS.md requires of every test added. Both reviewers were right to push on it and my earlier answer leaned on the file's pre-existing serial tests rather than the rule for new ones. defaultLogger now carries the hook. A nil one — the zero value a directly constructed logger has, which this package's tests rely on in a dozen places — still means os.Exit, so no construction site had to change, and WithContext carries it to the derived logger. Both tests set it on their own logger and are parallel. Removing the exit from the suppressed-level branch still fails the first test, and dropping the hook from WithContext fails its new assertion. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NcsBE6AGr9Ey8cpaiELUs8
This comment has been minimized.
This comment has been minimized.
fasthttp abandons a cookie at the first attribute it cannot read, so the earlier fix reset the cookie and reparsed the name=value pair alone. That kept the cookie alive but threw away everything already accepted: for "sid=x; Secure; HttpOnly; Path=/api; Expires=bogus" the parser had Secure, HttpOnly and Path in hand, and Response.Cookies() reported a cookie carrying none of them — dropping the security attributes silently, which the comment claimed it did not. Each attribute is now offered against the ones kept so far, so those on either side of a bad one survive, and only a name=value pair that will not parse fails the cookie. RFC 6265 §5.2 ignores an unparsable attribute, not the rest of them. The existing test now asserts Secure, HttpOnly and Path survive alongside the ignored Expires; it failed on Secure before the fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NcsBE6AGr9Ey8cpaiELUs8
Server.MaxConnsPerIP makes fasthttp wrap every accepted connection, and the wrapper is what the request side sees. underlyingConn matched the concrete *tls.Conn, so it did not unwrap, the lookup key differed from the one GetClientInfo stored, and ClientHelloInfo() returned nil — or worse, fell through to the app-wide last handshake this change exists to stop. Matching the promoted NetConn method unwraps both shapes. forget cannot do the same. fasthttp closes its wrapper and returns it to a pool before reporting the close, clearing the embedded connection on the way, so asking the wrapper for what it wrapped there panics on a nil *tls.Conn — an end-to-end test over TCP with MaxConnsPerIP crashed the server on the first close. It now unwraps only a live *tls.Conn, which leaves the record to be dropped when the connection object comes back rather than at close; documented on ClientHelloInfo. That test fails with "<nil>" for the first connection's server name before the unwrap fix. A unit test covers the wrapper shape directly. Also corrects the timeout middleware's limitation note: handleTimeout reclaims a *fiber.DefaultCtx once the handler and Fiber have both finished with it, so only a custom Ctx still leaks one per timed-out request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NcsBE6AGr9Ey8cpaiELUs8
This comment has been minimized.
This comment has been minimized.
core.execute released a client-created request when a pre- or post-hook failed, but not when execFunc itself returned an error: a dial failure, a transport error, a canceled context. No response reaches the caller on that path either, so nothing carries the request back to the pool and the helpers cannot release it themselves. A streamed body is left alone. The transport goroutine is handed that reader as-is rather than a copy, so a canceled send may still be reading it and releasing here would close it mid-read — the ownership question raised separately on execFunc, which needs its own change. The new test drives a failing dial and fails with "the failing send strands it" before the fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NcsBE6AGr9Ey8cpaiELUs8
…' into claude/codebase-bug-audit-g0uzel
|
@codex review |
There was a problem hiding this comment.
💡 Codex Security Review
Here are some automated security review suggestions for this pull request.
Reviewed commit: 8a56e9c4db
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8a56e9c4db
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…wrapper GetClientInfo records a ClientHelloInfo under the raw connection crypto/tls hands it, while the server sees whatever fasthttp accepted. With Server.MaxConnsPerIP set that is a pooled wrapper, and forget could only unwrap a concrete *tls.Conn: it deleted the wrapper's own key and left the raw connection and its ClientHelloInfo in the map forever. Sequential handshakes therefore grew the map without bound — the per-IP cap limits how many connections run at once, not how many are made. Report a connection to the handler when it is new as well as when it closes. While it is open it can still say what it wraps, so the note taken then resolves the record at close, whatever fasthttp handed over. A wrapper found still holding a note has been recycled — only Close returns one to the pool — so the record it would otherwise strand is dropped as the note is replaced. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NcsBE6AGr9Ey8cpaiELUs8
…heir end Three findings in code this branch introduced. expiresNowMaxAge stopped at the first Max-Age attribute while fasthttp keeps the last, so `sid=x; Max-Age=3600; Max-Age=0` was retained: fasthttp reported MaxAge 0, which is also what an absent attribute reports, and the raw scan said 3600. RFC 6265 resolves a repeated attribute by its last occurrence and ignores one whose value is not an integer, so the scan now reads them all and is the single source of the answer, replacing the reading fasthttp cannot give. Converting Max-Age to a duration overflowed: `Max-Age=10000000000` fits an int but not the nanoseconds of a time.Duration, so a cookie asking for three centuries of life was given an expiry in the past and deleted. The conversion now saturates at the longest duration it can express. CopyContextToFiberContext bounded its walk at 64 to stop a cyclic context chain, and silently dropped the outermost values of a net/http middleware stack deeper than that. Depth cannot tell a cycle from a deep chain, so it now remembers the contexts it has walked and stops only where it would repeat. Identity is the pointer with its type: an embedded first field shares the address of the struct holding it, and context's own types nest that way. Also says in the etag docs why a non-GET request whose If-None-Match matches passes through rather than being answered 412. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NcsBE6AGr9Ey8cpaiELUs8
Overlap check against the other open PRsI went through all 14 other open PRs to see whether anything here already fixes them, so nothing gets merged twice or dropped. Nothing does — every one of them fixes a genuinely different bug, including the ones that touch the same files. Recording the checks so a reviewer does not have to redo them:
The rest are clearly unrelated to anything here: #4653 (CORS/CSRF origin matching), #4642 ( Merge-order heads-upThree of the four above will conflict textually with this branch, so whoever merges second has to resolve it:
Happy to rebase onto whichever of these lands first, or to hold this one — say which order you would like. Generated by Claude Code |
The router performance work on main (#4655) reached path.go's import block, which this branch had also touched. Only the imports conflicted: swar is kept for the new slash scan, while fasthttp is dropped because this branch replaced fasthttp.AppendUnquotedArg with unescapePath — a path is not a form argument, and the form decoder turned a literal '+' into a space (RFC 3986 Section 3.3). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NcsBE6AGr9Ey8cpaiELUs8
|
Went through this branch against Three that look blocking1. The guard compares the decoded, normalised path ( Trigger set: any percent-escape including a no-op one ( 2. Mixed-case constraint names stop enforcing ( Three entry points: 3.
Every WebSocket upgrade over TLS hijacks. The one-line fix below covers only part of it: with PatchCloses 1, 2, the Verified: all six bypass spellings back to 404,
|
Three that block, each reproduced first.
adaptor.HTTPMiddleware resumed the chain past middleware that had already run.
The guard compared the decoded path against the raw one the router matched, and
those differ for every escaped or non-canonical request, so c.Path re-bucketed
the tree and c.Next resumed after the adaptor's own index: a guard mounted
before it never ran for /api%2Fadmin/secret, //admin/secret, /./admin/secret,
/x/../admin/secret or /adm%69n/secret. Only a genuine rewrite of r.URL now
re-routes, and the path is cloned before SetRequestURI overwrites the storage
r.URL still aliases.
A mixed-case constraint name stopped enforcing. The pattern is now adopted as
written, where it used to arrive lowercased, so <INT> and <Guid> resolved to no
handler at all and matched anything. Built-in names fold case; custom ones keep
the exact match, so two customs differing only in case stay distinct and a
custom cannot capture a built-in name in another case.
A hijacked connection stranded its ClientHelloInfo. StateHijacked is terminal
in fasthttp — StateClosed never follows it — so every WebSocket upgrade over
TLS retained a record.
And four more the same pass turned up.
Two concurrent Shutdown callers each terminated every service, with a data race
between them: the started list was copied, and a service is only removed once it
has terminated, too late to stop the second call. Shutdown now drains the list
under its own mutex and puts back only what would not terminate, so a later
shutdown still retries those. The mutex released during the drain stays
released; holding it deadlocks against in-flight handlers.
Group.Use and domainRouter.Use mounted a sub-app under the first prefix only,
the same bug already fixed for App.Use.
sameFS compared two file systems backed by one array as equal, because a
slice's pointer is the address of its first element and ignores the length.
Max-Age was parsed into an int, which is 32 bits on 386 and arm, so a value past
two billion failed to parse and silently downgraded a persistent cookie to a
session cookie.
The auto-HEAD memo was written from a defer, so a panicking OnRoute hook
recorded an aborted scan as complete and left HEAD at 405 for the lifetime of
the process.
Also corrects two documentation lines that no longer described the code: the
adaptor's context walk is bounded by cycle detection rather than 64 levels, and
SendFile{Compress: false} is not what turns off the compress middleware.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NcsBE6AGr9Ey8cpaiELUs8
…' into claude/codebase-bug-audit-g0uzel
|
Thank you — this is the most useful review the PR has had, and the three blocking findings are all real. Fixed in The three that block1. Adaptor resumed past middleware that already ran. Confirmed, and it is the worst thing in the PR: an auth guard mounted before the adaptor never ran. Your diagnosis is exact — the guard compared 2. Mixed-case constraint names. Confirmed — 8 of the 10 spellings I tested returned 200 for input the constraint should reject, with 3. On the deeper point — "this component has produced three defects across three correction rounds" — you are right and I am not going to patch it a fourth time on my own judgement. The remaining hole you name (records retained when fasthttp refuses at the per-IP limit before Four more from your list, also fixed
Your four numbers: you are right on all of themI checked each against the merge base rather than argue.
Also fair on the test wording: "each written before its fix" covers more than it can, and 104-of-141 is the honest figure. Still open, and I am not claiming otherwiseTaking these in a follow-up rather than growing this diff further, unless you would rather they land here:
Full suite green under Generated by Claude Code |
equalFieldType runs for every key of every bound request, and the walk that replaced main's single map lookup cost 1.60x on Benchmark_equalFieldType against the merge base. Most of that was not the walk: it was the sync.Map holding each type's field information, whose interface-keyed hash ran up to four times per call, plus reflect.Value construction and repeated Kind() calls that the type alone could have answered. A small direct-mapped cache now sits in front of the sync.Map, keyed by the address of the type descriptor — stable and unique for the life of the program, and read straight from the interface's data word, since going through reflect.ValueOf for the same word cost more than the lookup it served. A collision only falls through to the map. The per-tag cache is resolved once per call rather than per level, equalFieldType walks the type instead of a reflect.Value (a Value is taken only to tell a nil map from one the decoder can fill), and structKeyKind carries each level's kind instead of asking the type for it again. Interleaved medians against the merge base on one machine, allocations unchanged at zero: 1.60x → 1.06x, within the benchmark's own run-to-run spread. One behavioral consequence: a typed nil pointer to a struct now gets the same answer main gave it — what kind the named field has — rather than false; the nil check was this branch's addition and cost a reflect.Value walk on every call to decide a case the decoder rejects anyway. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NcsBE6AGr9Ey8cpaiELUs8
|
| base | head | ratio | |
|---|---|---|---|
| before | 150.5 ns | 240 ns | 1.60x |
| front cache | 150.5 ns | 210 ns | 1.37x |
| + type walk, kind tracking | 150.5 ns | 176 ns | 1.17x |
| + direct descriptor read | 150.5 ns | 160 ns | 1.06x |
The residual is within the benchmark's own run-to-run spread here (base alone ranged 148–159 across rounds). The bot's re-measurement on its own hardware is the arbiter; I am not claiming parity from one machine again.
One behavioral note. A typed nil pointer to a struct now gets the answer main gives it — what kind the named field has — rather than false. That nil check was this branch's addition, cost a reflect.Value walk on every call, and decided a case the decoder rejects anyway.
Race-clean, lint 0 issues, full suite green. Two tests that poison the type cache to exercise the mismatch path now also clear the slot in front of it, so they keep testing what they claim to.
Generated by Claude Code
|
✅ No significant benchmark change. 51c0085 vs main@ac861c5 · 1692/1692 results compared · retest: 1 reported re-checked · noise-aware thresholds · full results |
main's #4652 landed a per-registration Route.id and an auto-HEAD scan skip that overlap with this branch's own bookkeeping, so the two had to be reconciled rather than taken side by side: - Route.regID is gone. main's id is the same notion — one value per register() call, shared by that call's per-method entries — so the scoped helpers key off it and the hot struct keeps a single counter. App.registrationID gives way to main's process-wide routeIDs, which also removes the cross-app collision the clone in processSubAppsRoutes used to clear its id to avoid. - Auto-HEAD twins keep the id they copy from their GET route, because routeIndexInTree finds a route in another method's tree by it. Documentation is kept off them by their autoHead flag instead, which is what the stack scan in applyToRegIDLocked now skips. - Twin OnRoute hooks still fire unlocked, so a sub-app hook may call back into the parent. To keep main's guarantee that an aborted pass is retried, fireOnRouteHooks clears the scan markers unless every hook returned, and RebuildTree fires the twins it used to discard. - App.Name keeps this branch's registration-scoped form, which already covers the id match main added and still names the GET route's HEAD twin. - copyRoute keeps the single-struct-copy form: it preserves every field main lists, id included, and additionally clones the documentation containers. - buildTree returns nothing now that RebuildTree no longer forwards its result. Verified on the merged tree: build, vet, golangci-lint (0 issues), go test ./..., and -race on the core and openapi packages. openapi coverage unchanged at 97.3%. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XHncVMdy1VPWCTp53PQoYG
Description
This PR is the result of a correctness audit of the codebase. It fixes 70+ distinct bugs across the app lifecycle, the router,
Ctx/request/response handling, the binder, the REST client and twelve middlewares, together with the documentation that described the old (or intended-but-unimplemented) behavior.Each fix has a regression test that reproduces the bug against the unfixed code. The branch adds 162 tests; the full suite (
make test, race detector, shuffled) passes.No single issue tracks these; each fix is described below with the regression test that pins it.
Bugs fixed
App lifecycle, TLS and services
ShutdownWithContextdeadlocked against its own handlers. The app mutex was held for the whole connection drain, so an in-flight handler that takes the mutex itself (RebuildTree,RemoveRoute,Name) could never finish and everyShutdownWithTimeoutran to its deadline. The mutex is now released before draining. (Test_App_ShutdownWithTimeout_HandlerTakesAppMutex)Shutdowncallers then terminated every service twice, racing each other while doing it. The started list was handed out as a copy, and a service is only removed once it has terminated — too late to stop the second call. Shutdown now drains the list under its own mutex and puts back only what would not terminate, so a later shutdown still retries those. (Test_ShutdownServices_ConcurrentCallersTerminateOnce)net.IPform, so2001:DB8::1,2001:0db8::1or0:0:0:0:0:0:0:1never matched. Addresses are now stored canonically, with the IPv4-mapped spelling registered alongside. (Test_App_IsProxyTrusted_CanonicalIPForms,Test_App_IP_StripTrustedProxies_CanonicalIPForms)Ctx.ClientHelloInforeported the wrong handshake.TLSHandlerkept one app-wideClientHelloInfo, so under concurrency a handler saw whichever TLS handshake happened most recently on any connection. The handler now records one per connection and releases it when the server reports the connection closed, via aConnStatehook that preserves a user-installed callback. (Test_Listener_TLS_ClientHelloInfo_PerConnection,Test_Listener_ConnState_UserCallbackPreserved)MaxConnsPerIP, because the pooled wrapper fasthttp installs for per-IP accounting is closed, cleared and recycled before the close is reported. A connection is now reported to the handler while it is still open, and the note taken then resolves its record at close. (Test_Listener_TLS_ClientHelloInfo_MaxConnsPerIP_NoLeak,Test_TLSHandler_ForgetsRecycledWrapper)StateHijackedis terminal in fasthttp —StateClosednever follows it — so every WebSocket upgrade over TLS retained a record. (Test_TLSHandler_ForgetsHijackedConnection) See Notes for reviewers for the remaining gap in this component and the redesign it needs.GracefulContextconfigured, an explicitShutdownmadeListenreturn, which woke the graceful-shutdown goroutine into a second shutdown; aListenthat failed before serving fired the hooks for nothing. (Test_Listen_GracefulShutdown_HooksOnce,Test_Listen_GracefulShutdown_NoHooksOnListenError)ShutdownTimeoutwas not applied as documented. The default is now applied on the built-in shutdown path, and a negative value disables the timeout and waits indefinitely. (Test_ListenConfigDefault_ShutdownTimeout,Test_GracefulShutdown_NegativeTimeoutWaitsIndefinitely)CertFile/CertKeyFilefell through to the plain-HTTP branch. It now fails withErrCertFileAndKeyRequired. (Test_Listen_TLS_PartialCertConfig)State, and termination order was random since the started services were iterated from a map rather than in reverse start order. (Test_StartServices_DuplicateNames,Test_ShutdownServices_ReverseStartOrder)App.ErrorHandlernow returns without running the configured handler for such a request; the error still reaches outer middleware. (Test_App_ErrorHandler_SkipsCommittedTimeoutResponse,Test_Timeout_ErrorHandlerSkippedAfterTimeout)Router
CaseSensitiveoff the whole pattern was lowercased before parsing, constraint names and arguments included:regex([A-Z]{2})matched lowercase only,\Dbecame\d, a datetime layout lost itsTandZ, and a mixed-case custom constraint was dropped altogether. Constraints are now adopted from the pattern as written. (Test_Router_Constraints_CaseInsensitiveRouting)<INT>and<Guid>resolved to no handler at all and matched anything. Built-in names now fold case; custom ones keep the exact match, so two customs differing only in case stay distinct and a custom cannot capture a built-in name in another case. (Test_Router_Constraints_MixedCaseNames)Nextresumed at the old bucket's index inside the new tree, skipping the endpoints before it. Routes now carry a registration id so their position can be found in the other tree. (Test_Router_MethodOverride_ContinuesInNewMethodTree,Test_Router_PathOverride_ContinuesInNewBucket)/\*is a literal*in the path; the star flag is now derived from the pattern with its escapes intact. (Test_Router_EscapedStar_IsLiteral)StrictRoutingwas not applied to parametric routes, and not applied at all to the routes of a mounted app./a/:id/matched/a/x, while the equivalent static route correctly did not. (Test_Router_StrictRouting_ParamTrailingSlash,Test_App_Mount_StrictRouting)UnescapePathturned a literal+into a space, because the path was decoded with fasthttp's form-argument decoder. In a path+is an ordinary character (RFC 3986 §3.3); malformed escapes are now also left as-is instead of mangled. (Test_Router_UnescapePath_PlusIsLiteral,Test_UnescapePath_MalformedEscape)Nameapplied to only one route of a multi-methodAdd. (Test_App_Add_MultipleMethods_Name)Usewith a prefix slice and a sub-app mounted only the first prefix — inApp.Use, and inGroup.UseanddomainRouter.Use, which the first round missed. (Test_App_Use_MultiplePrefixes_MountsEachPrefix,Test_Group_Use_MultiplePrefixes_MountsEachPrefix,Test_Domain_Use_MultiplePrefixes_MountsEachPrefix)Test_App_Mount_Self_Panics)RebuildTree; a sub-app mounted onto an already-mounted app (discovered only after the auto-HEAD pass had run); and after a panickingOnRoutehook, because the "scan complete" memo was written from adeferand recorded the aborted scan, leaving HEAD at 405 for the lifetime of the process. (Test_App_RebuildTree_RegistersAutoHead,Test_Mount_Nested_AutoHead_SingleStartup,Test_App_AutoHead_PanickingHookLeavesScanIncomplete)Ctx, request and response
SendFilepanicked on an uncomparablefs.FS. The handler cache comparedfs.FSinterface values with==, which panics for a map-backed type such asfstest.MapFSon the second call. Such values are now compared by the identity of what they reference — including the length of a slice-backed one, since two prefixes of one array share an element pointer — and a func-backed file system is treated as distinct rather than risk serving the wrong root. (Test_Ctx_SendFile_UncomparableFS,Test_SameFS,Test_SameFS_SliceLength)SendFiledeleted the request'sAccept-Encoding, so middleware running after it (compress, logger) no longer saw what the client accepts. (Test_Ctx_SendFile_KeepsAcceptEncoding,Test_Ctx_SendFile_CompressedByMiddleware)AutoFormatansweredtext/htmlwith noAcceptheader, where it is documented to fall back totext/plain. (Test_Ctx_AutoFormat_NoAcceptHeader)*ortext/*) must not outrank a specific range that gives the offer a lower one — or rejects it withq=0. The resolution is now specificity-aware, with a fast path for uniform-weight headers and a first-match shortcut. (Test_Ctx_Accepts_MostSpecificRangeSetsQuality,Test_Utils_GetOffer_QualityZeroRejection)::ffff:a.b.c.d(RFC 4291 §2.2); the validator rejected that spelling because it carries both.and:, soX-Forwarded-Forwas discarded and the proxy itself was returned asc.IP(). (Test_Ctx_IP_IPv4MappedIPv6InProxyHeader)Formathandlers or custom binders, so ac.(*MyCtx)assertion that holds in a route handler failed there. (Test_Ctx_Format_CustomCtx,Test_Bind_CustomBinder_CustomCtx)identitystep of a coding chain re-set the request body with a slice aliasing it, which underReduceMemoryUsagereturned the buffer to fasthttp's pool while the next decoder was still reading from it. (Test_Ctx_Body_With_Compression_IdentityChain)middleware/adaptororapp.Handleron a hand-madeRequestCtxdoes not carry. (Test_Redirect_Messages_ProgrammaticCookieHeader)Test_EqualFieldType_*/Test_QueryBinder_Bind_Splitting_*/Test_HeaderBinder_*tests,Test_fieldName,Test_StructKeyKind_NestedPaths)Client
Max-Ageor anExpiresin another date format aborted cookie parsing; RFC 6265 §5.2 says an unparsable attribute is ignored, so the cookie's name and value are now kept. (Test_Client_UnparsableCookieAttributesIgnored,Test_Client_ResponseCookie_UnparsableAttribute)Request, so the documentedAcquireRequest/ReleaseRequestpattern released it twice — while a request the client itself created for the call was stranded when a hook failed. Requests a client helper creates are now tracked separately from caller-owned ones. (Test_Client_ResponseHookError_KeepsCallerRequest,Test_Request_Reset_ClearsClient,Test_Client_PreHookError_ReleasesOwnedRequest,Test_Client_AfterHookError_ReleasesOwnedRequest)Max-Age. AMax-Age=0logout kept sending the cookie and a shortMax-Agenever expired it.Max-Agenow takes precedence overExpires(RFC 6265 §5.2.2), while a value that is not an integer is ignored rather than treated as an immediate expiry. (Test_CookieJar_MaxAge,Test_CookieJar_MaxAgePrecedence)Max-Ageby the wrong one, overflowed on a large one, and truncated one on 32-bit. fasthttp keeps the last occurrence while the attribute scan stopped at the first;Max-Age=10000000000fits anintbut not the nanoseconds of atime.Duration, so a cookie asking for three centuries of life was given an expiry in the past; and parsing intointtruncates on 386 and arm, where a value past two billion silently downgraded a persistent cookie to a session cookie. (Test_CookieJar_MaxAgePrecedence,Test_CookieJar_MaxAgeBeyond32Bit)Sendhad already returnedErrTimeoutOrCancel, by which time the caller may have released it. (Test_Request_CancelledContext_NoRace, race detector)Test_Client_RequestHeaderOverridesClientHeader,Test_Request_Resend_DoesNotAccumulateHeaders)Test_Client_Debug_DoesNotDrainStream,Test_Client_Logger_StreamedBodiesLogHeadersOnly)Response.Headerreturned a string aliasing pooled memory, invalid after the response was released. (Test_Response_Header_CopiesValue)sort.Sortis not stable). (Test_Request_Params_KeepInsertionOrder)Test_Client_UppercaseScheme)Middleware
cache
Test_Cache_StorageMissingBodyIsMiss,Test_Cache_OrphanedMetadataWithoutBody,Test_Cache_BodyFetchFailure)no-cacherequest left the old heap node behind: it popped in a later eviction and deleted the live entry, while its bytes stayed counted againstMaxBytesforever. (Test_Cache_NoCacheRefresh_KeepsAccounting)Test_Cache_VaryManifestStoreFailureUnreservesSpace)Test_Cache_UncacheableResponseDoesNotEvict)206 Partial Contentwas cached and replayed — status, range body and all — to the next request for the whole representation. (Test_Cache_PartialContentNotStored)Age. (Test_CacheAgeClockStepsBackwards)CacheInvalidatormarked the shared in-memory item expired under the lock while concurrent hits read its expiry after dropping it — a data race the detector reports. (Test_CacheInvalidator_SharedEntryNoDataRace)limiter / storage — a positive
Expirationbelow one second fell back to the one-minute default; the in-memory storages truncated TTLs to whole seconds, so a sub-second TTL was stored already expired and1.9sexpired after one. TTLs now round up. (Test_Limiter_SubSecondExpirationIsOneSecondWindow,Test_Memory_TTLRoundsUp,Test_Memory_SubSecondTTLExpires,Test_Storage_Memory_Set_ExpirationRoundsUp,Test_Storage_Memory_GCInterval_SubSecond)session —
Resetwiped the absolute expiration along with the data, so a rotated session never expired absolutely. (Test_Session_Reset_KeepsAbsoluteTimeout)etag —
304 Not Modifiedwas returned for methods other than GET and HEAD, where RFC 9110 §13.1.2 does not define it and the handler has already run. (Test_ETag_IfNoneMatchOnlyForGetHead)keyauth — the
WWW-Authenticatechallenge was skipped when theErrorHandlerreturned a*fiber.Errorfor the app to write instead of setting the status itself. (Test_ErrorHandlerReturnsErrorChallenge)timeout — the default path returned
nil, so outer middleware recorded a timed-out request as a success; and a*fiber.Errorreturned byOnTimeoutwas dropped in favor of the default 408. (Test_Timeout_DefaultReturnsErrRequestTimeout,Test_Timeout_OnTimeoutReturnedErrorShapesResponse)proxy — after
Forwardthe app's own request still carried the upstreamHost; andDomainForwardended the chain with an empty 200 for a non-matching host instead of passing it on, while aHostcarrying a port never matched at all. (Test_Proxy_Forward_RestoresHost,Test_Proxy_DomainForward_NonMatchingHostContinues,Test_Proxy_HostWithoutPort)static — one handler registered on two routes captured the first request's prefix and stripped it from every route; the
Downloadattachment header was set before the file lookup and leaked onto the fallthrough response of a missing file;DownloadderivedContent-Typefrom the URL extension, blanking the type detected for an extension-less file; andMaxAgewas applied to error responses such as416. (Test_Static_SameHandlerUnderTwoPrefixes,Test_Static_Download_NotFoundLeavesNoAttachment,Test_Static_Download_KeepsDetectedContentType,Test_Static_MaxAge_NotOnErrorResponses)compress — negotiation used fasthttp's exact token matching, ignoring weights, wildcards and lists without spaces (so
gzip;q=0still compressed with gzip, and*matched nothing); an element carrying parameters other than the weight, or an unparsableq, was read as a rejection instead of taking the default weight of 1 per RFC 9110 §12.4.2; recomputing a strongETagdrained a streamed body into memory; and the client's ownAccept-Encodingheader was left mutated. (Test_Compress_AcceptEncoding_Negotiation,Test_Compress_StreamedBody_KeepsETagWeak,Test_Compress_RequestHeaderUntouched)sse — a reconnection delay below one millisecond was written as
retry: 0, telling the client to reconnect immediately. (Test_SSE_Retry_SubMillisecondRoundsUp)adaptor — the context-value walk only recursed into a field literally named
Context, so every value below aWithTimeout,WithDeadline,WithoutCancelorAfterFunccontext was lost; a depth bound meant to stop a cyclic chain silently dropped the outermost values of a deeper middleware stack, so the walk now follows the chain to its end and stops only where it would repeat, keyed on each context's pointer with its type; a request net/http served over TLS was seen as plaintext by Fiber; onlyRequestURIwas copied back after middleware ran, so a rewrite ofr.URLsuch ashttp.StripPrefixwas ignored; and the guard installing that rewritten path compared the decoded path against the raw one the router matched, so any escaped or non-canonical request re-bucketed the tree and resumed the chain past middleware that had already run — an auth guard mounted before the adaptor never ran for/api%2Fadmin/secretand four other spellings. (Test_CopyContextToFiberContext_DerivedContexts,Test_CopyContextToFiberContext_DeepChain,Test_CopyContextToFiberContext_Cycle,Test_HTTPHandler_TLSPropagated,Test_HTTPHandler_TLSConnectionStateCarried,Test_HTTPMiddleware_URLRewrite,Test_HTTPMiddleware_NoRewriteKeepsChainPosition,Test_HTTPMiddleware_NextCalledAfterReturn)retry (addon) —
ExponentialBackoffmutated its owncurrentInterval, so a secondRetrycall on the same value started atMaxBackoffTimeand concurrent callers corrupted each other's backoff. (Test_ExponentialBackoff_Retry_StartsFromInitialInterval,Test_ExponentialBackoff_Retry_Concurrent)log —
Fatalreturned before exiting when the logger level suppressed its output, so a fatal condition let the program continue. (Test_DefaultLogger_FatalExitsAboveLevel,Test_DefaultLogger_FatalExitsAfterWrite)paginate — with no
SortKeyconfigured the middleware still readc.Query(""), letting a nameless?=-nameparameter control the sort order; andNextPageURLcould overflow. (Test_Paginate_EmptySortKey_IgnoresQuery,Test_PageInfo_NextPageURL_NoOverflow)favicon — the read limit was computed as
MaxBytes+1, which overflowed formath.MaxInt64and served the icon as an empty response. (Test_Favicon_MaxBytes_MaxInt64)Changes introduced
Benchmarks: two benchmarks moved, both measured against the merge base (
b192a98) on one machine.Benchmark_Utils_GetOffer— the negotiation fix is on a hot path. Medians of-count=5, allocations unchanged in every case:simple6_offers1_parameter2_parameters3_parameters10_parameters6_offers_w/paramsmime_extension(×4)web_browserUniform-weight headers take a fast path and are unchanged within noise. The
web_browsercase is the one that genuinely costs more: mixed weights require checking whether a more specific range demotes the first match, which is exactly the bug being fixed. @ReneWerner87 has sketched a way to recover roughly half of that; it is on the follow-up list below.Benchmark_equalFieldType— ~1.06x against the merge base after51c0085(interleaved medians on one machine: 150.5 → 160 ns/op), allocations unchanged at 0. The correct walk down the struct that replaced main's single map lookup first cost 1.60x; profiling put most of that not in the walk but around it — thesync.Mapholding each type's field information, whose interface-keyed hash ran up to four times per call, plusreflect.Valueconstruction and repeatedKind()calls. A small direct-mapped cache keyed by the type descriptor's address now sits in front of the map (a collision only falls through to it), the per-tag cache is resolved once per call rather than per level, and the type is walked instead of areflect.Value. The residual is within the benchmark's own run-to-run spread, and the CI bot's re-measurement on51c0085agrees: no significant change across all 1692 compared benchmarks, so the gate is green andBenchmark_equalFieldTypeis no longer flagged.Benchmark_App_RebuildTree— 22888 → 33050 B/op (+44.4%), 75 → 84 allocs, deterministic. The cause is legitimate: the auto-HEAD fix takes the route count from 239 to 381, so this is the price of registering the companions that were previously missing.One earlier regression the CI bot flagged was a real bug, since fixed and back to parity:
Benchmark_ShutdownServices_.../successful-completion(121 ms → 61 ms —App.initalready starts configured services, so a secondstartServicesregistered each twice and shutdown terminated them twice).Patch coverage: the fixes add guard and fallback branches, so the branch's own new lines were measured and the reachable ones covered: 57 → 14 uncovered statements. Measuring them turned up the compress default-weight bug above. The 14 that remain are unreachable without corrupt state or two simultaneous storage faults.
Documentation Update: 21 documentation pages were corrected to match the fixed behavior. This also documents three previously undocumented behaviors found during the audit:
ShutdownTimeoutsemantics, thatGracefulContextandShutdown*act only on the process they run in under prefork, and thatResponse.Closereleases the request only when the client created it.Changelog/What's New: Bug fixes only; no API additions.
Migration Guide: Not required — no API changes. Behavior changes are corrections toward the documented and RFC-specified behavior. One deserves a release note:
proxy.DomainForwardnow passes a non-matching host on to the routes behind it instead of ending the chain, which removes a host-isolation property some deployments may lean on.API Alignment with Express: No new API surface.
API Longevity: No exported signatures changed. The only new exported symbol is the
ErrCertFileAndKeyRequirederror value.Examples: No new features to demonstrate.
Type of change
The template offers no "bug fix" option; this PR is bug fixes plus the documentation corrections that go with them.
Checklist
/docs/directory.make test: 5769 tests, race detector on,-shuffle=on.)Notes for reviewers
Deliberately not changed here, called out rather than silently left:
/. Pinned by existing tests; changing it would be a breaking routing change and belongs in its own PR.SkipUnmatchedRoutesanswers 405 before the middleware chain, so a method-override middleware never runs for those requests. This is the documented short-circuit; a test now pins it.etagdoes not answer an unsafe method with412. RFC 9110 §13.1.2 calls for 412 where a matchingIf-None-Matcharrives on a method other than GET or HEAD, but that rule assumes the precondition is evaluated before the method is applied — §15.5.13 defines 412 by that purpose. This middleware runs afterc.Next(), over a200 OKthe handler already produced, so answering 412 would tell the client its request was not performed when it was.docs/middleware/etag.mdnow says this.ClientHelloInfomap still has one hole: records are retained when fasthttp refuses a connection at the per-IP limit beforeStateNewfires. The design assumesStateNewfires for every connection that completes a handshake, which is not true on every path. What removes the class rather than patching it again is storing the info on a Fiber-owned listener wrapper so cleanup rides onClose(). That is a redesign and is proposed as a follow-up.Follow-ups from @ReneWerner87's review, agreed and not in this diff: the
sameFS(x, x)SendFilestore/goroutine leak for non-comparable structs, a per-mountRoute.idremap, thecompress/client allocation regressions and theBenchmark_Compressfixture reusing oneRequestCtx, and the negotiation fast-path recovery. Two pre-existing issues they found on both trees will be filed separately: thereq.go:192SetBodyRawaliasing race, and root-package test-order instability under-shuffle=on.make betteralignreports two findings (routeParserinpath.go,Routeinrouter.go) that reproduce identically on the merge base. Both structs carry existing "field order is load-bearing" comments and//nolint:govet // fieldalignmentdirectives, so their layout was left alone.🤖 Generated with Claude Code
https://claude.ai/code/session_01NcsBE6AGr9Ey8cpaiELUs8