🐛 fix(cache): structured default cache keys, and controls#4224
🐛 fix(cache): structured default cache keys, and controls#4224ReneWerner87 merged 22 commits intomainfrom
Conversation
|
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:
WalkthroughCache middleware now uses structured, method-prefixed cache keys (path, canonical query, selected headers/cookies) with canonicalization, size limits and hashing fallbacks; adds config toggles for query and Vary inclusion; expands tests and docs; and adds GoDoc notes that Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant CacheMW as Cache Middleware
participant KeyGen as defaultKeyGenerator
participant Store as Cache Store
participant Origin as Origin Handler
Client->>CacheMW: Request(method, path, query, headers, cookies)
CacheMW->>KeyGen: Build key (method|path|query|headers|cookies)
KeyGen-->>CacheMW: Structured key (escaped/hashed segments)
CacheMW->>Store: Lookup(key [, vary-manifest])
alt cache hit & valid
Store-->>CacheMW: cached response
CacheMW-->>Client: response (hit)
else miss or uncacheable
CacheMW->>Origin: Call handler
Origin-->>CacheMW: response (+Vary header)
CacheMW->>Store: Store response (partition by Vary if enabled)
CacheMW-->>Client: response (miss)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 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 #4224 +/- ##
==========================================
- Coverage 91.24% 91.17% -0.08%
==========================================
Files 123 123
Lines 11910 12076 +166
==========================================
+ Hits 10867 11010 +143
- Misses 655 668 +13
- Partials 388 398 +10
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Code Review
This pull request hardens the cache middleware's key generation strategy by moving from a path-only approach to a structured key that includes the request method, canonicalized query strings, and selected headers or cookies. It introduces several new configuration options like KeyHeaders, KeyCookies, DisableQueryKeys, and DisableVaryHeaders while removing the configurable Methods option in favor of a fixed GET and HEAD restriction. Feedback suggests improving performance by using a sync.Pool for buffer allocations in the key generator and optimizing the normalization of key dimensions. Additionally, it was noted that the method check condition could be simplified for better readability.
There was a problem hiding this comment.
Pull request overview
This PR hardens the cache middleware by switching the default cache key from path-only to a structured, deterministic key derived from multiple request dimensions, while adding config controls to tune key partitioning and Vary handling. It also updates tests and documentation to reflect the new defaults and clarifies End() behavior in streaming scenarios.
Changes:
- Introduces structured default cache key dimensions (method partitioning, path, canonical query, selected headers/cookies) and adds config toggles (
DisableQueryKeys,KeyHeaders,KeyCookies,DisableVaryHeaders). - Restricts cache participation to
GET/HEADrequests and updates Vary manifest usage based onDisableVaryHeaders. - Updates cache tests and docs to match the new behavior and adds interface documentation notes about
End()and streaming.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| middleware/cache/cache.go | Implements the new structured default key generation and conditional Vary behavior; changes method eligibility to GET/HEAD only. |
| middleware/cache/config.go | Adds new key-dimension config fields, updates defaults, and normalizes key dimension lists. |
| middleware/cache/cache_test.go | Updates tests to align with new key semantics and GET/HEAD-only caching; adds new key-dimension coverage. |
| docs/middleware/cache.md | Documents the new default key behavior and new config options. |
| docs/whats_new.md | Notes the breaking/default behavior changes for cache keys and request method handling. |
| ctx_interface_gen.go | Adds a note that End() doesn’t work in streaming modes. |
| res_interface_gen.go | Adds a note that End() doesn’t work in streaming modes. |
|
@copilot apply changes based on the comments in this thread |
Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/4d59616d-e436-43a7-901e-b82f34511b2b Co-authored-by: gaby <[email protected]>
Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/4d59616d-e436-43a7-901e-b82f34511b2b Co-authored-by: gaby <[email protected]>
Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/4d59616d-e436-43a7-901e-b82f34511b2b Co-authored-by: gaby <[email protected]>
Applied the requested cache review fixes in f56bfa1. The update bounds long default path segments, preserves case-sensitive |
There was a problem hiding this comment.
Pull request overview
This PR hardens the cache middleware’s default cache-key generation to avoid path-only collisions and adds explicit configuration knobs to control which request dimensions partition cached responses, with corresponding test and documentation updates.
Changes:
- Introduces a structured default cache key (method + path + canonical query + selected headers/cookies), plus key-dimension normalization and key-segment bounding.
- Changes default caching behavior to only apply to
GET/HEADand adds options to disable query keying and Vary-based partitioning. - Updates tests and docs to reflect the new defaults and configuration surface.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| middleware/cache/cache.go | Implements structured default key generation, GET/HEAD-only caching, and conditional Vary-manifest handling. |
| middleware/cache/config.go | Adds new config fields and normalizes header/cookie key dimensions; wires default key generator when KeyGenerator is nil. |
| middleware/cache/cache_test.go | Updates existing expectations for new key format/defaults and adds coverage for key-dimension behavior and bounded segments. |
| docs/middleware/cache.md | Documents the new default key behavior and new configuration fields; updates examples/config table. |
| docs/whats_new.md | Notes the cache-key hardening and new cache configuration options in release notes. |
| ctx_interface_gen.go | Regenerates interface docs to include the End() streaming caveat. |
| res_interface_gen.go | Regenerates interface docs to include the End() streaming caveat. |
|
@copilot apply changes based on the comments in this thread |
Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/ff791c7d-7251-4fe1-b09e-e9bbd4db4f8c Co-authored-by: gaby <[email protected]>
There was a problem hiding this comment.
Pull request overview
This PR hardens the cache middleware’s default key generation to avoid collisions (especially from query strings), adds config knobs for controlling which request dimensions partition cache entries, and updates docs/tests accordingly.
Changes:
- Replaced path-only default cache keys with a structured key built from method + path + canonical query + selected headers/cookies (with bounded segments and hashing).
- Added configuration controls for query/header/cookie partitioning and for disabling Vary-based partitioning.
- Expanded/updated test suite and documentation to reflect safer defaults and new options.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
middleware/cache/cache.go |
Implements structured default key generation, canonicalization helpers, and Vary handling controls. |
middleware/cache/config.go |
Introduces new key-dimension config fields and normalization helpers; updates defaults. |
middleware/cache/cache_test.go |
Updates existing tests for new key format/defaults and adds key-dimension tests. |
middleware/cache/cache_security_test.go |
Adds security-focused tests for DoS bounds and key-dimension behaviors. |
docs/middleware/cache.md |
Documents new defaults and config options for key partitioning and Vary behavior. |
docs/whats_new.md |
Notes the breaking/default behavior changes and new configuration surface. |
ctx_interface_gen.go |
Adds clarification to End() about streaming-mode limitations. |
res_interface_gen.go |
Adds clarification to End() about streaming-mode limitations. |
|
@claude[agent] apply changes based on the comments in this thread |
…middleware Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/d1ccaef2-2286-4f6d-8485-9f45185bd718 Co-authored-by: gaby <[email protected]>
All security issues have been addressed in commit 047de64: DoS Protection (Issue #1): Added pre-scan to Delimiter Injection (Issues #4 & #5): Implemented Documentation (Issue #2): Added clarification to Test Coverage (Issue #3): Added three new comprehensive tests:
All tests pass and linter checks are clean. |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
middleware/cache/cache.go (1)
595-604: Simplify redundantDisableVaryHeadersgating.
shouldStoreVaryManifestalready includes!cfg.DisableVaryHeaders, so the repeated conjunctions on lines 596, 600, and 845 are redundant. Collapsing them improves readability and keeps the invariant in one place.♻️ Proposed simplification
- shouldStoreVaryManifest := !cfg.DisableVaryHeaders && len(varyNames) > 0 - if !cfg.DisableVaryHeaders && len(varyNames) > 0 { + shouldStoreVaryManifest := !cfg.DisableVaryHeaders && len(varyNames) > 0 + if shouldStoreVaryManifest { if key == baseKey { key += buildVaryKey(varyNames, &c.Request().Header) } } else if !cfg.DisableVaryHeaders && hasVaryManifest {And at line 845:
- if !cfg.DisableVaryHeaders && shouldStoreVaryManifest { + if shouldStoreVaryManifest { if err := storeVaryManifest(reqCtx, manager, manifestKey, varyNames, storageExpiration); err != nil { return err } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@middleware/cache/cache.go` around lines 595 - 604, The gating on cfg.DisableVaryHeaders is redundant because shouldStoreVaryManifest already embeds !cfg.DisableVaryHeaders; update the conditions to use shouldStoreVaryManifest instead of repeating !cfg.DisableVaryHeaders && len(varyNames) > 0 (affecting the block that mutates key when key == baseKey and the else-if that checks hasVaryManifest). Replace those repeated conjunctions with shouldStoreVaryManifest where appropriate (referencing variables: shouldStoreVaryManifest, varyNames, key, baseKey, buildVaryKey) and keep the stale-manifest deletion branch to only run when !shouldStoreVaryManifest && hasVaryManifest (using manager.del, manifestKey, maskKey) so logic and behavior remain unchanged but the DisableVaryHeaders check is centralized.middleware/cache/config.go (1)
129-164: Closure inconfigDefaultcaptures a pointer to the localcfg, not the returned copy.cfg := config[0] // ... if cfg.KeyGenerator == nil { cfg.KeyGenerator = func(c fiber.Ctx) string { return defaultKeyGenerator(c, &cfg) // &cfg == address of the local, heap-escaped } } return cfg // struct copy; closure still references the originalIn practice this is fine because callers don't mutate the returned
ConfigafterconfigDefault, and at return time the local and the copy are value-equal. But it is a subtle footgun: if a caller ever doescfg := configDefault(...); cfg.KeyHeaders = ..., the default key generator will keep using the pre-mutation values. Consider either documenting this or snapshottingKeyHeaders/KeyCookiesexplicitly in the closure (e.g.keyHeaders := cfg.KeyHeaders; keyCookies := cfg.KeyCookies; ... defaultKeyGenerator(c, &snapshotCfg)), so the generator is clearly bound to the config observed at construction time.Non-blocking — optional hardening.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@middleware/cache/config.go` around lines 129 - 164, configDefault's closure captures the local cfg variable so subsequent mutations to the returned Config won't be reflected as expected; fix by snapshotting the config before creating the closure: create a local copy (e.g. snapshot := cfg) and set KeyGenerator to use &snapshot (or capture individual slices like keyHeaders := cfg.KeyHeaders, keyCookies := cfg.KeyCookies and pass those) instead of &cfg, and apply this change in both the "no config provided" branch and the "override default config" branch where KeyGenerator is assigned, ensuring the closure binds to the immutable snapshot observed at construction time.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@middleware/cache/cache_security_test.go`:
- Around line 54-89: The test Test_Cache_Security_DoS_ExcessiveQueryBuffer
currently builds a query string that stays below the guard maxQueryBufferSize
and therefore doesn't exercise the fallback in canonicalQueryString; increase
the payload so the post-escape query length exceeds maxQueryBufferSize
(reference canonicalQueryString and maxQueryBufferSize) by enlarging
specialChars (e.g., use a much larger strings.Repeat count such as 200+ or 500+)
and/or increasing the number of queryParams (e.g., 50+), then keep the same
assertions to verify the code hits the buffer-limit path and still returns
correct cache headers and a single handler invocation in the hit case.
In `@middleware/cache/cache.go`:
- Around line 1278-1314: The path segment appended in defaultKeyGenerator
currently uses boundKeySegment(c.Path()) without escaping delimiter characters;
update defaultKeyGenerator to pass the bounded path through escapeKeyDelimiters
(i.e., use escapeKeyDelimiters(boundKeySegment(c.Path()))) before appending so
path characters like '|' or ':' cannot collide with the key structure—mirror how
canonicalHeaderSubset and canonicalCookieSubset are used for headers/cookies to
keep escaping consistent with canonicalQueryString and avoid structural
ambiguity.
---
Nitpick comments:
In `@middleware/cache/cache.go`:
- Around line 595-604: The gating on cfg.DisableVaryHeaders is redundant because
shouldStoreVaryManifest already embeds !cfg.DisableVaryHeaders; update the
conditions to use shouldStoreVaryManifest instead of repeating
!cfg.DisableVaryHeaders && len(varyNames) > 0 (affecting the block that mutates
key when key == baseKey and the else-if that checks hasVaryManifest). Replace
those repeated conjunctions with shouldStoreVaryManifest where appropriate
(referencing variables: shouldStoreVaryManifest, varyNames, key, baseKey,
buildVaryKey) and keep the stale-manifest deletion branch to only run when
!shouldStoreVaryManifest && hasVaryManifest (using manager.del, manifestKey,
maskKey) so logic and behavior remain unchanged but the DisableVaryHeaders check
is centralized.
In `@middleware/cache/config.go`:
- Around line 129-164: configDefault's closure captures the local cfg variable
so subsequent mutations to the returned Config won't be reflected as expected;
fix by snapshotting the config before creating the closure: create a local copy
(e.g. snapshot := cfg) and set KeyGenerator to use &snapshot (or capture
individual slices like keyHeaders := cfg.KeyHeaders, keyCookies :=
cfg.KeyCookies and pass those) instead of &cfg, and apply this change in both
the "no config provided" branch and the "override default config" branch where
KeyGenerator is assigned, ensuring the closure binds to the immutable snapshot
observed at construction time.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 1ee35d84-12ee-4cf2-99a9-646e82690839
📒 Files selected for processing (8)
ctx_interface_gen.godocs/middleware/cache.mddocs/whats_new.mdmiddleware/cache/cache.gomiddleware/cache/cache_security_test.gomiddleware/cache/cache_test.gomiddleware/cache/config.gores_interface_gen.go
- Restore configurable Methods field (default: GET, HEAD) to replace hardcoded method check, with uppercase normalization and nil vs empty-slice semantics (nil = default, [] = disable caching) - Fix escapeKeyDelimiters fast-path bug: backslash was not checked, allowing collisions between literal "\p" and escaped "|" - Fix path delimiter injection: escape pipe/colon/backslash in request path before boundKeySegment to prevent crafted paths from manipulating cache key structure - Optimize canonicalQueryString: add single-param fast path (skips url.ParseQuery/sort) and use sync.Pool for output buffer - Simplify string conversions: replace utils.CopyString(utils.UnsafeString(buf)) with string(buf) and utils.UnsafeBytes(boundKeySegment(...)) with direct string append - Add comprehensive tests: Methods config (POST caching, bypass, empty-slice, lowercase normalization), escapeKeyDelimiters unit regression test with collision-pair verification - Update docs: Methods field in config table, default config, and migration guide
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@middleware/cache/config.go`:
- Around line 168-177: The cfg.Methods slice is being mutated in place causing
caller-side side effects; before normalizing HTTP method names in the config
initialization (the cfg.Methods handling that currently assigns
ConfigDefault.Methods or uppercases in-place), make a defensive copy of
cfg.Methods when it is non-nil (e.g., newSlice := append([]string(nil),
cfg.Methods...)) and operate on that copy so the caller's slice isn't modified,
and use utilsstrings.ToUpper instead of strings.ToUpper to match existing
helpers (ensure ConfigDefault.Methods stays unchanged when cfg.Methods is nil).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: abbe9c4c-7361-4b4f-bcd4-6d382c5158d4
📒 Files selected for processing (6)
docs/middleware/cache.mddocs/whats_new.mdmiddleware/cache/cache.gomiddleware/cache/cache_security_test.gomiddleware/cache/cache_test.gomiddleware/cache/config.go
✅ Files skipped from review due to trivial changes (1)
- middleware/cache/cache_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
- docs/whats_new.md
- middleware/cache/cache_security_test.go
There was a problem hiding this comment.
♻️ Duplicate comments (1)
middleware/cache/config.go (1)
167-176:⚠️ Potential issue | 🟡 MinorCaller's
Methodsslice is still mutated in place.The
utilsstrings.ToUpperswap was applied, but the defensive copy wasn't. Sincecfg := config[0]at line 153 is a shallow copy,cfg.Methodsshares the backing array with the caller's slice, so the loop at lines 173–175 writes back into the caller's storage. A caller passing[]string{"get","head"}will observe it mutated to["GET","HEAD"]aftercache.New(...).🛠️ Proposed fix
if cfg.Methods == nil { cfg.Methods = ConfigDefault.Methods } else { // Normalize method names to uppercase (HTTP methods are case-sensitive - // and c.Method() returns uppercase, e.g. "GET" not "get") - for i, m := range cfg.Methods { - cfg.Methods[i] = utilsstrings.ToUpper(m) - } + // and c.Method() returns uppercase, e.g. "GET" not "get"). + // Copy first to avoid mutating the caller's slice. + normalized := make([]string, len(cfg.Methods)) + for i, m := range cfg.Methods { + normalized[i] = utilsstrings.ToUpper(m) + } + cfg.Methods = normalized }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@middleware/cache/config.go` around lines 167 - 176, The Methods slice is being mutated in place (cfg.Methods) when normalizing to uppercase, which also mutates the caller's slice because cfg := config[0] is a shallow copy; to fix, make a defensive copy of cfg.Methods before normalizing (allocate a new []string, copy elements, assign back to cfg.Methods), preserve the special-case nil => ConfigDefault.Methods behavior, and then run utilsstrings.ToUpper on the copied slice (reference symbols: cfg.Methods, ConfigDefault.Methods, utilsstrings.ToUpper, config[0]).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@middleware/cache/config.go`:
- Around line 167-176: The Methods slice is being mutated in place (cfg.Methods)
when normalizing to uppercase, which also mutates the caller's slice because cfg
:= config[0] is a shallow copy; to fix, make a defensive copy of cfg.Methods
before normalizing (allocate a new []string, copy elements, assign back to
cfg.Methods), preserve the special-case nil => ConfigDefault.Methods behavior,
and then run utilsstrings.ToUpper on the copied slice (reference symbols:
cfg.Methods, ConfigDefault.Methods, utilsstrings.ToUpper, config[0]).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: ceb63f3d-bfd9-44af-bc2f-1e92517ffe42
📒 Files selected for processing (1)
middleware/cache/config.go
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@middleware/cache/cache_test.go`:
- Line 5454: In middleware/cache/cache_test.go replace the invalid token in the
failing test tuple {"partial match", "no-cach", "no-cache", false} with a valid
word that should still not match "no-cache" (e.g., change "no-cach" to
"no-catch"); update the second element of that test case so the test string is a
real token but remains unequal to "no-cache".
In `@middleware/cache/cache.go`:
- Around line 1338-1340: The fast-path and fallback returns in
middleware/cache/cache.go currently return raw query text (e.g., the
single-parameter fast path that calls boundKeySegment(query) and the parse/limit
fallback branches), which bypasses delimiter escaping and canonicalization;
update the code so that before returning any query-derived segment you run the
same canonicalization/escaping routine used for long-query segments (i.e., pass
the query through the canonicalize/escape function) and then call
boundKeySegment or return that escaped value—apply this change to the single-key
fast path, the parse/limit fallback branches, and any other places where raw
query is returned to ensure no collisions with hashed segments.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: dcb01800-7631-4467-b519-92c37542fb92
📒 Files selected for processing (3)
docs/middleware/cache.mdmiddleware/cache/cache.gomiddleware/cache/cache_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
- docs/middleware/cache.md
Motivation
/?id=1vs/?id=2) while keeping deterministic, bounded key sizes.Vary).Description
defaultKeyGeneratorthat builds keys from request method (partitioned internally), path, canonicalized query string, configured header subset (KeyHeaders), and explicit cookie subset (KeyCookies), with long segments hashed viaboundKeySegment.Configfields:DisableQueryKeys,KeyHeaders,KeyCookies, andDisableVaryHeaders, removed the oldMethodslist in favor of caching onlyGET/HEADby default, and madeKeyGeneratornil by default to use the structured generator.normalizeKeyDimensions), and protections for long key segments; updated Vary handling to respectDisableVaryHeadersand avoid storing/using Vary manifests when disabled.KeyGenerator, guidance on default key behavior, and a note inCtx/Resinterfaces documenting thatEnd()does not work in streaming modes.