Skip to content

feat: tag invalidation, conditional requests, and Cache-Control for cache middleware#4041

Closed
zishanjawed wants to merge 1 commit into
gofiber:mainfrom
zishanjawed:feat/cache-tag-invalidation-conditional-cachecontrol
Closed

feat: tag invalidation, conditional requests, and Cache-Control for cache middleware#4041
zishanjawed wants to merge 1 commit into
gofiber:mainfrom
zishanjawed:feat/cache-tag-invalidation-conditional-cachecontrol

Conversation

@zishanjawed
Copy link
Copy Markdown

Summary

  • Tag-based cache invalidation with in-memory and distributed (shared-storage) backends
  • RFC 9110 conditional request validation (ETag / Last-Modified / 304)
  • Automatic Cache-Control header generation with handler-override preservation
  • Full test coverage including concurrent and cross-instance scenarios
  • Benchmarks for all tag operations and encoding paths
  • README documenting tag strategies, invalidation, conditional requests, and Cache-Control

Test plan

  • go test -race ./middleware/cache/ passes clean
  • Tag association and invalidation via InvalidateTags works end-to-end
  • Cross-instance invalidation: two distributedTagStore instances sharing one storage backend; invalidation on one finds keys written by the other
  • Conditional 304 responses: ETag auto-generation, custom generator, If-None-Match; Last-Modified auto-generation, custom generator, If-Modified-Since
  • RFC 9110 §8.3 precedence: If-None-Match takes priority when both conditional headers are present
  • Cache-Control: middleware-generated header on miss and hit; handler-set header preserved verbatim; DisableCacheControl suppresses generation
  • RejectTags prevents storage for matching tag patterns (exact, prefix, general glob)
  • Benchmarks: has() is O(1) across index sizes; distributed has() matches in-memory cost; invalidation scales linearly with keys per tag

🤖 Generated with Claude Code

…ache middleware

- Tag association via Tags (request-time) and ResponseTags (response-time) hooks
- Bidirectional tag index with O(1) lookup; automatic distributed backend when
  Config.Storage is set, persisting the index in the shared store so that
  InvalidateTags propagates across all instances
- Tag re-population from persisted entry data on cache hit (survives restarts)
- RejectTags glob-pattern matching with pre-classified exact/prefix/general buckets
- InvalidateTags public API: collects keys from the tag index, evicts from heap,
  and deletes from storage
- ETag: auto-generation (SHA-256 body hash) and custom ETagGenerator; weak
  If-None-Match validation on hit and miss paths
- Last-Modified: auto-generation and custom LastModifiedGenerator;
  If-Modified-Since validation with RFC 9110 §8.3 precedence
- Cache-Control: automatic "public, max-age=<N>[, must-revalidate]" on miss and
  hit; handler-set Cache-Control stored and replayed verbatim
- Benchmarks covering has(), add(), invalidate(), and encode/decode
- README documenting tag strategies, invalidation patterns, conditional requests,
  and Cache-Control configuration

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@zishanjawed zishanjawed requested a review from a team as a code owner January 31, 2026 13:48
@welcome
Copy link
Copy Markdown

welcome Bot commented Jan 31, 2026

Thanks for opening this pull request! 🎉 Please check out our contributing guidelines. If you need help or want to chat with us, join us on Discord https://gofiber.io/discord

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 31, 2026

Walkthrough

This PR extends the cache middleware with tag-based invalidation, RFC 9110 conditional request support (ETag/Last-Modified), and distributed tag store coordination. It introduces a new public API for tag invalidation, tag computation hooks, rejection patterns, and automatic conditional header generation with fallback generators.

Changes

Cohort / File(s) Summary
Documentation
middleware/cache/README.md
Comprehensive guide covering tag strategies, tag index backends, rejection patterns, invalidation workflows, conditional request handling (RFC 9110), Cache-Control configuration, and usage examples.
Core API & Configuration
middleware/cache/cache.go, middleware/cache/config.go
New public API InvalidateTags() for tag-based invalidation; new Config fields (EnableETag, EnableLastModified, Tags, ResponseTags, RejectTags, ETagGenerator, LastModifiedGenerator); internal utilities for ETag/Last-Modified generation and Cache-Control header building; tag-store initialization and integration on store/evict paths; conditional request handling with 304 responses.
Internal State Management
middleware/cache/manager.go, middleware/cache/manager_msgp.go
Added lastModified (uint64) and tags ([]string) fields to cache item struct; extended MsgPack serialization/deserialization paths to support new fields with proper encoding, decoding, and size calculations.
Tag System Implementation
middleware/cache/tags.go, middleware/cache/tags_distributed.go
In-memory tagIndex with bidirectional tag↔key mappings; rejectMatcher for pattern-based tag rejection (exact, prefix, glob); distributedTagStore for cross-instance coordination via shared storage with forward/reverse indices, synchronized mutations, and local fast-path caching.
Tests & Benchmarks
middleware/cache/tags_test.go, middleware/cache/tags_bench_test.go, middleware/cache/tags_distributed_test.go
Comprehensive unit and integration tests for tagIndex, rejectMatcher, glob matching, tag-based caching, tag merging, rejection, ETag/Last-Modified generation, conditional requests (304), Cache-Control preservation, concurrent access, and distributed tag store coordination; benchmarks for tag operations and encoding/decoding.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Handler as Handler/App
    participant Middleware as Cache Middleware
    participant TagIndex as Tag Index
    participant Storage as Storage Backend
    
    Client->>Handler: GET /resource (no cache)
    Handler->>Middleware: Process request
    Middleware->>Middleware: Tags(ctx) - compute request tags
    Handler->>Handler: Execute handler
    Middleware->>Middleware: ResponseTags(ctx, body) - compute response tags
    Middleware->>Middleware: Merge Tags + ResponseTags
    Middleware->>Middleware: Check RejectTags patterns
    alt Not rejected
        Middleware->>Middleware: ETagGenerator(ctx, body)
        Middleware->>Middleware: LastModifiedGenerator(ctx)
        Middleware->>TagIndex: Add key with tags
        Middleware->>Storage: Store item + metadata
    end
    Middleware->>Client: Response + headers (ETag, Last-Modified, Cache-Control)
Loading
sequenceDiagram
    participant Client
    participant Middleware as Cache Middleware
    participant Storage as Storage Backend
    participant TagIndex as Tag Index
    participant DistStore as Distributed TagStore
    
    Client->>Middleware: InvalidateTags(c, tag1, tag2)
    Middleware->>Middleware: Retrieve invalidation function from Locals
    Middleware->>DistStore: Invalidate(tag1, tag2)
    DistStore->>Storage: Read forward indices for tag1, tag2
    DistStore->>DistStore: Gather all keys across tags
    DistStore->>Storage: Delete forward indices
    DistStore->>Storage: Read & delete reverse indices for each key
    DistStore->>TagIndex: Update local index
    Middleware->>Middleware: Evict gathered keys from cache
    Middleware->>Client: Success
Loading
sequenceDiagram
    participant Client
    participant Middleware as Cache Middleware
    participant Cache as Cache Store
    
    Client->>Middleware: GET /resource (If-None-Match: "abc123")
    Middleware->>Cache: Lookup key
    alt Cache hit
        Middleware->>Middleware: Retrieve stored ETag
        alt ETag matches (weak or strong)
            Middleware->>Middleware: Build 304 response
            Middleware->>Middleware: Apply Cache-Control (must-revalidate, proxy-revalidate)
            Middleware->>Client: 304 Not Modified + headers
        else ETag mismatch
            Middleware->>Client: 200 + full response
        end
    else Cache miss
        Middleware->>Client: Forward to handler
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

📜 RFC Compliance

Suggested reviewers

  • gaby
  • ReneWerner87
  • sixcolors

Poem

🐰 Tags align like garden rows,
Cache keys dancing where invalidation flows,
ETags whisper "still the same,"
Conditional requests—a caching game! ✨
Distributed stores in perfect sync,
One hop, two hops, faster than a blink! 🚀

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.12% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main features added: tag invalidation, conditional requests, and Cache-Control handling for the cache middleware.
Description check ✅ Passed The description addresses most template sections with concrete details on features, test coverage, and documentation, though some optional sections like examples and migration guide are not included.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @zishanjawed, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly upgrades the Fiber cache middleware by integrating advanced caching strategies. It introduces a flexible tag-based invalidation system that works seamlessly across single-instance and distributed environments, allowing developers to group and invalidate cache entries logically. Furthermore, it enhances HTTP compliance by adding comprehensive support for RFC 9110 conditional requests, including ETag and Last-Modified header management, which reduces unnecessary data transfers. The middleware also gains intelligent Cache-Control header generation and processing, ensuring optimal cache behavior and adherence to web standards.

Highlights

  • Tag-Based Cache Invalidation: Introduced a robust tag-based cache invalidation system, supporting both in-memory and distributed storage backends (e.g., Redis) for cluster-wide invalidation. This allows for flushing related content with a single call using cache.InvalidateTags.
  • RFC 9110 Conditional Request Validation: Implemented full support for RFC 9110 conditional requests, including automatic ETag generation (SHA-256 hash of response body) and Last-Modified header generation. The middleware now handles If-None-Match and If-Modified-Since headers, returning 304 Not Modified responses to optimize bandwidth.
  • Automatic Cache-Control Header Management: Added automatic generation of Cache-Control headers (e.g., public, max-age=...) on cache misses and hits, while preserving any Cache-Control headers explicitly set by the handler. Directives like no-store, no-cache, s-maxage, and must-revalidate are now respected.
  • Tag Rejection Mechanism: A new RejectTags configuration option allows defining glob patterns to prevent responses from being cached if their associated tags match any of the rejection patterns.
  • Enhanced Cache Entry Persistence: Cache entries now persist associated tags and Last-Modified timestamps, enabling transparent recovery of the tag index across process restarts and accurate conditional request handling.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces significant new features to the cache middleware, including tag-based invalidation, conditional request support (ETag/Last-Modified), and improved Cache-Control header handling, with comprehensive documentation and testing. However, a critical vulnerability has been identified in the distributedTagStore due to race conditions, which can lead to inconsistent cache states and failed invalidations in multi-instance deployments. Further security concerns include potential memory exhaustion and Denial of Service risks from manual binary decoding and unbounded tag set growth. Additionally, there are medium-severity issues related to performance in the glob matching logic and robustness in ETag parsing. Addressing these concerns will make this an excellent addition to the Fiber framework.

Comment on lines +37 to +61
func (d *distributedTagStore) add(key string, tags []string) {
if len(tags) == 0 {
return
}
d.local.add(key, tags)

d.mu.Lock()
defer d.mu.Unlock()

for _, tag := range tags {
fwdKey := tagKeyPrefix + tag
fwd := d.readSet(fwdKey)
if !sliceContains(fwd, key) {
d.writeSet(fwdKey, append(fwd, key))
}
}
revKey := tagRevKeyPrefix + key
rev := d.readSet(revKey)
for _, tag := range tags {
if !sliceContains(rev, tag) {
rev = append(rev, tag)
}
}
d.writeSet(revKey, rev)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The distributedTagStore.add method is vulnerable to race conditions in a distributed environment. The use of a local sync.Mutex is insufficient, as concurrent read-modify-write operations across multiple instances will overwrite each other's updates, leading to a corrupted tag index. This can result in inconsistent cache states and failed invalidations, posing a security risk where sensitive or deleted data might persist in the cache. This issue also affects the remove and invalidate methods. To mitigate this, consider using atomic operations provided by your storage backend or implementing a distributed locking mechanism.

Comment on lines +172 to +173
count := binary.LittleEndian.Uint32(data[0:4])
ss := make([]string, 0, count)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The decodeStringSet function reads a 32-bit count from the input data and uses it to pre-allocate a slice with make([]string, 0, count). If the storage backend contains malformed or malicious data with a very large count, this will trigger a massive memory allocation, potentially leading to a Denial of Service (DoS) via an Out-Of-Memory (OOM) crash. Since each string entry in the encoded format requires at least 4 bytes for its length, the count should be validated against the total length of the data before allocation.

count := binary.LittleEndian.Uint32(data[0:4])
	if count > uint32(len(data))/4 {
		return nil
	}
	ss := make([]string, 0, count)

Comment on lines +37 to +61
func (d *distributedTagStore) add(key string, tags []string) {
if len(tags) == 0 {
return
}
d.local.add(key, tags)

d.mu.Lock()
defer d.mu.Unlock()

for _, tag := range tags {
fwdKey := tagKeyPrefix + tag
fwd := d.readSet(fwdKey)
if !sliceContains(fwd, key) {
d.writeSet(fwdKey, append(fwd, key))
}
}
revKey := tagRevKeyPrefix + key
rev := d.readSet(revKey)
for _, tag := range tags {
if !sliceContains(rev, tag) {
rev = append(rev, tag)
}
}
d.writeSet(revKey, rev)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The distributedTagStore.add method has O(N) time and space complexity relative to the number of keys associated with a tag, as it must read and write the entire set of keys for every addition. Since there is no limit on the number of keys per tag, an attacker can associate a large number of unique cache keys with a single tag (e.g., by generating many unique URL paths). This will cause the tag's key set to grow indefinitely, making every subsequent request that uses that tag increasingly slow and resource-intensive, eventually leading to a Denial of Service.

Comment thread middleware/cache/cache.go
header = header[end+1:]
}

candidate = strings.TrimRight(candidate, " ")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using strings.TrimRight here only removes trailing whitespace from the ETag candidate. The If-None-Match header can have leading whitespace as well (e.g., "etag1", "etag2"). To make the parsing more robust, you should trim whitespace from both sides.

I suggest changing this to strings.TrimSpace(candidate).

Suggested change
candidate = strings.TrimRight(candidate, " ")
candidate = strings.TrimSpace(candidate)

Comment thread middleware/cache/tags.go
Comment on lines +203 to +207
for i := range len(s) + 1 {
if globMatch(pattern, s[i:]) {
return true
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This recursive implementation of globMatch creates a new string slice s[i:] in each iteration of the loop. For long strings or complex patterns, this can lead to a significant number of allocations and performance degradation.

Consider rewriting this function to be non-recursive or to use indices instead of slicing to avoid these allocations. The standard library's path.Match uses an iterative approach which could serve as a good reference.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
middleware/cache/config.go (1)

81-194: ⚠️ Potential issue | 🟠 Major

EnableETag/EnableLastModified defaults drop to false when Config is provided.
configDefault doesn’t populate these fields, so cache.New(cache.Config{Expiration: ...}) turns both features off despite the documented default of true. This will surprise most callers who pass a partial config.

Consider using pointers (or explicit Disable* flags) so defaults remain true unless explicitly overridden.

💡 One possible fix (pointer-based defaults)
 type Config struct {
-	EnableETag bool
+	EnableETag *bool
-	EnableLastModified bool
+	EnableLastModified *bool
 }
 
 var ConfigDefault = Config{
-	EnableETag:         true,
-	EnableLastModified: true,
+	EnableETag:         boolPtr(true),
+	EnableLastModified: boolPtr(true),
 }
 
 func configDefault(config ...Config) Config {
 	if len(config) < 1 {
 		return ConfigDefault
 	}
-	cfg := config[0]
+	cfg := ConfigDefault
+	user := config[0]
 	// existing defaulting...
+	if user.EnableETag != nil {
+		cfg.EnableETag = user.EnableETag
+	}
+	if user.EnableLastModified != nil {
+		cfg.EnableLastModified = user.EnableLastModified
+	}
 	return cfg
 }
+
+func boolPtr(v bool) *bool { return &v }

(You’d also need to update cache.go call sites to dereference these pointers.)

🤖 Fix all issues with AI agents
In `@middleware/cache/cache.go`:
- Around line 530-542: The conditional-request logic currently falls back to
evaluating If-Modified-Since when If-None-Match is present but the cached item
has no ETag; change the control flow in the block that computes notModified (and
the analogous block around lines 701-712) so that If-Modified-Since is only
considered when If-None-Match is absent (i.e., check ifIfNoneMatch header first
and if it's non-empty then only evaluate etagWeakMatch with e.etag, otherwise if
If-None-Match is empty evaluate If-Modified-Since using fasthttp.ParseHTTPDate
and secondsToTime(e.lastModified)); ensure you reference and preserve existing
helpers etagWeakMatch, secondsToTime and fields e.etag and e.lastModified when
updating the logic.
- Around line 1129-1132: When registering tags for a cached key (where the code
currently calls ti.add(key, entryTags)), first clear any existing tag mappings
for that key to avoid stale associations; update the logic around ti.add so it
either calls a removal method (e.g., ti.remove(key) or ti.deleteKeyTags(key))
before ti.add, or use/implement a single replace-style method (e.g.,
ti.replace(key, entryTags) or ti.setTags(key, entryTags)) that atomically clears
old tags and adds the new set; ensure this change is applied where key and
entryTags (and ResponseTags) are used so re-caching replaces prior tag mappings
instead of appending.

In `@middleware/cache/README.md`:
- Around line 145-152: Several fenced code blocks in the README lack language
identifiers; update each triple-backtick fence to include a language (use
```text for plain text snippets and ```go for Go code examples) so MD040 is
satisfied—specifically change the block containing the sample starting with
Entry "k1" tagged ["product:1", "region:us"] to ```text and likewise update the
other similar blocks referenced (the snippets around the same example and the
blocks near the other occurrences) to the appropriate language tag.
- Around line 20-23: Several Markdown tables in middleware/cache/README.md
(e.g., the table with headers "Hook", "When it runs", "Signature" and rows for
`Tags` and `ResponseTags`) have incorrect spacing around pipe characters causing
MD060; update all affected tables (lines referenced in the review: around rows
for `Tags`/`ResponseTags` and the other ranges) to include single spaces before
and after each pipe in every row and header so they conform to markdownlint
table spacing rules, then run the repository lint target (`make markdown`) to
normalize formatting and ensure no remaining MD060 violations.
🧹 Nitpick comments (1)
middleware/cache/tags_distributed.go (1)

166-186: Guard decodeStringSet against oversized counts.
count comes from storage and is used as slice capacity; corrupted data can trigger huge allocations. Consider validating count against available bytes before allocating.

🛡️ Suggested defensive check
 	count := binary.LittleEndian.Uint32(data[0:4])
+	maxCount := uint32((len(data) - 4) / 4) // each entry needs at least a uint32 len
+	if count > maxCount {
+		return nil
+	}
 	ss := make([]string, 0, count)

Comment thread middleware/cache/cache.go
Comment on lines +530 to +542
// Check conditional request headers (RFC 7232).
// ETag and Last-Modified are in the cached item; no body load needed for 304.
{
ifNoneMatch := c.Request().Header.Peek(fiber.HeaderIfNoneMatch)
ifModSince := c.Request().Header.Peek(fiber.HeaderIfModifiedSince)
notModified := false
if len(ifNoneMatch) > 0 && len(e.etag) > 0 {
notModified = etagWeakMatch(ifNoneMatch, e.etag)
} else if len(ifModSince) > 0 && e.lastModified != 0 {
if modTime, parseErr := fasthttp.ParseHTTPDate(ifModSince); parseErr == nil {
notModified = !secondsToTime(e.lastModified).After(modTime)
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

If-None-Match should suppress If-Modified-Since even when ETag is missing.
Right now the code falls back to If-Modified-Since when If-None-Match is present but no ETag is stored, which violates RFC 9110 §8.3 (and the README). Evaluate IMS only when If-None-Match is absent.

✅ Suggested fix for both hit and miss paths
-					if len(ifNoneMatch) > 0 && len(e.etag) > 0 {
-						notModified = etagWeakMatch(ifNoneMatch, e.etag)
-					} else if len(ifModSince) > 0 && e.lastModified != 0 {
+					if len(ifNoneMatch) > 0 {
+						if len(e.etag) > 0 {
+							notModified = etagWeakMatch(ifNoneMatch, e.etag)
+						}
+					} else if len(ifModSince) > 0 && e.lastModified != 0 {
 						if modTime, parseErr := fasthttp.ParseHTTPDate(ifModSince); parseErr == nil {
 							notModified = !secondsToTime(e.lastModified).After(modTime)
 						}
 					}
-			if len(ifNoneMatch) > 0 && len(storedETag) > 0 {
-				notModified = etagWeakMatch(ifNoneMatch, storedETag)
-			} else if ifModSince := c.Request().Header.Peek(fiber.HeaderIfModifiedSince); len(ifModSince) > 0 {
+			if len(ifNoneMatch) > 0 {
+				if len(storedETag) > 0 {
+					notModified = etagWeakMatch(ifNoneMatch, storedETag)
+				}
+			} else if ifModSince := c.Request().Header.Peek(fiber.HeaderIfModifiedSince); len(ifModSince) > 0 {
 				if lm := c.Response().Header.Peek(fiber.HeaderLastModified); len(lm) > 0 {
 					if modTime, parseErr := fasthttp.ParseHTTPDate(ifModSince); parseErr == nil {
 						if lmTime, lmErr := fasthttp.ParseHTTPDate(lm); lmErr == nil {
 							notModified = !lmTime.After(modTime)
 						}
 					}
 				}
 			}

Also applies to: 701-712

🤖 Prompt for AI Agents
In `@middleware/cache/cache.go` around lines 530 - 542, The conditional-request
logic currently falls back to evaluating If-Modified-Since when If-None-Match is
present but the cached item has no ETag; change the control flow in the block
that computes notModified (and the analogous block around lines 701-712) so that
If-Modified-Since is only considered when If-None-Match is absent (i.e., check
ifIfNoneMatch header first and if it's non-empty then only evaluate
etagWeakMatch with e.etag, otherwise if If-None-Match is empty evaluate
If-Modified-Since using fasthttp.ParseHTTPDate and
secondsToTime(e.lastModified)); ensure you reference and preserve existing
helpers etagWeakMatch, secondsToTime and fields e.etag and e.lastModified when
updating the logic.

Comment thread middleware/cache/cache.go
Comment on lines +1129 to +1132
// Register tags for this cache entry
if ti != nil && len(entryTags) > 0 {
ti.add(key, entryTags)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Refresh tag index when re-caching a key to avoid stale tag associations.
ti.add only appends new tags; if a key is re-cached with a different tag set (e.g., ResponseTags changes), old tags remain and future invalidations can evict the wrong entries. Clear existing mappings for the key before adding new tags.

🔁 Suggested update
-		if ti != nil && len(entryTags) > 0 {
-			ti.add(key, entryTags)
-		}
+		if ti != nil {
+			ti.remove(key)
+			if len(entryTags) > 0 {
+				ti.add(key, entryTags)
+			}
+		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Register tags for this cache entry
if ti != nil && len(entryTags) > 0 {
ti.add(key, entryTags)
}
// Register tags for this cache entry
if ti != nil {
ti.remove(key)
if len(entryTags) > 0 {
ti.add(key, entryTags)
}
}
🤖 Prompt for AI Agents
In `@middleware/cache/cache.go` around lines 1129 - 1132, When registering tags
for a cached key (where the code currently calls ti.add(key, entryTags)), first
clear any existing tag mappings for that key to avoid stale associations; update
the logic around ti.add so it either calls a removal method (e.g.,
ti.remove(key) or ti.deleteKeyTags(key)) before ti.add, or use/implement a
single replace-style method (e.g., ti.replace(key, entryTags) or ti.setTags(key,
entryTags)) that atomically clears old tags and adds the new set; ensure this
change is applied where key and entryTags (and ResponseTags) are used so
re-caching replaces prior tag mappings instead of appending.

Comment on lines +20 to +23
| Hook | When it runs | Signature |
|---|---|---|
| `Tags` | Before the handler, using only request data | `func(c fiber.Ctx) []string` |
| `ResponseTags` | After the handler, with access to the response body | `func(c fiber.Ctx, body []byte) []string` |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Markdown table spacing fails MD060.
markdownlint reports missing spaces around table pipes in multiple tables. Please reformat or run the markdown linter to normalize spacing.
As per coding guidelines: **/*.md: Run make markdown to lint all Markdown files when modifying code.

Also applies to: 47-51, 60-63, 97-101, 284-292, 320-327

🧰 Tools
🪛 markdownlint-cli2 (0.20.0)

[warning] 21-21: Table column style
Table pipe is missing space to the right for style "compact"

(MD060, table-column-style)


[warning] 21-21: Table column style
Table pipe is missing space to the left for style "compact"

(MD060, table-column-style)


[warning] 21-21: Table column style
Table pipe is missing space to the right for style "compact"

(MD060, table-column-style)


[warning] 21-21: Table column style
Table pipe is missing space to the left for style "compact"

(MD060, table-column-style)


[warning] 21-21: Table column style
Table pipe is missing space to the right for style "compact"

(MD060, table-column-style)


[warning] 21-21: Table column style
Table pipe is missing space to the left for style "compact"

(MD060, table-column-style)

🤖 Prompt for AI Agents
In `@middleware/cache/README.md` around lines 20 - 23, Several Markdown tables in
middleware/cache/README.md (e.g., the table with headers "Hook", "When it runs",
"Signature" and rows for `Tags` and `ResponseTags`) have incorrect spacing
around pipe characters causing MD060; update all affected tables (lines
referenced in the review: around rows for `Tags`/`ResponseTags` and the other
ranges) to include single spaces before and after each pipe in every row and
header so they conform to markdownlint table spacing rules, then run the
repository lint target (`make markdown`) to normalize formatting and ensure no
remaining MD060 violations.

Comment on lines +145 to +152
```
Entry "k1" tagged ["product:1", "region:us"]

InvalidateTags(c, "region:us")
→ k1 is evicted from the cache
→ forward index for "region:us" is cleared
→ reverse index for k1 retains ["product:1"]
```
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add language identifiers to fenced code blocks.
MD040 flags fenced blocks without a language; use text/go where appropriate.

Also applies to: 172-174, 254-256

🧰 Tools
🪛 markdownlint-cli2 (0.20.0)

[warning] 145-145: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In `@middleware/cache/README.md` around lines 145 - 152, Several fenced code
blocks in the README lack language identifiers; update each triple-backtick
fence to include a language (use ```text for plain text snippets and ```go for
Go code examples) so MD040 is satisfied—specifically change the block containing
the sample starting with Entry "k1" tagged ["product:1", "region:us"] to ```text
and likewise update the other similar blocks referenced (the snippets around the
same example and the blocks near the other occurrences) to the appropriate
language tag.

@gaby gaby closed this Jan 31, 2026
@github-project-automation github-project-automation Bot moved this to Done in v3 Jan 31, 2026
@ReneWerner87 ReneWerner87 removed this from v3 Feb 1, 2026
@ReneWerner87 ReneWerner87 removed this from the v3 milestone Feb 1, 2026
@ReneWerner87
Copy link
Copy Markdown
Member

same as #4038
We are still waiting for communication with the author.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants