Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 32 additions & 12 deletions middleware/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -907,21 +907,41 @@ func New(config ...Config) fiber.Handler {

// hasDirective checks if a cache-control header contains a directive (case-insensitive)
func hasDirective(cc, directive string) bool {
ccLen := len(cc)
dirLen := len(directive)
for i := 0; i <= ccLen-dirLen; i++ {
if !utils.EqualFold(cc[i:i+dirLen], directive) {
continue
start := 0
for start < len(cc) {
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

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

parseCacheControlDirectives([]byte(cc), ...) allocates a new byte slice on every call. Since the parser is read-only, prefer utils.UnsafeBytes(cc) (as used elsewhere in this file, e.g. parseMaxAge at cache.go:1086) to avoid the extra allocation in this hot path.

Suggested change
for start < len(cc) {
parseCacheControlDirectives(utils.UnsafeBytes(cc), func(key, _ []byte) {

Copilot uses AI. Check for mistakes.
end := start
for end < len(cc) && cc[end] != ',' {
end++
}
Comment on lines +910 to 915
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

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

parseCacheControlDirectives currently only treats ' ' as optional whitespace when trimming keys/parts. As a result, for inputs like "no-cache\t" the parsed key becomes "no-cache\t" and this comparison never matches, so the bug for tab-terminated directives persists (and the new regression test will fail). Update the directive parser (or this call site) to treat \t as OWS everywhere it trims/skips whitespace (leading separators, key/value trimming).

Copilot uses AI. Check for mistakes.
if i > 0 {
prev := cc[i-1]
if prev != ' ' && prev != ',' {
continue
}

partStart, partEnd := start, end
for partStart < partEnd && (cc[partStart] == ' ' || cc[partStart] == '\t') {
partStart++
}
if i+dirLen == ccLen || cc[i+dirLen] == ',' {
return true
for partEnd > partStart && (cc[partEnd-1] == ' ' || cc[partEnd-1] == '\t') {
partEnd--
}

if partStart < partEnd {
keyEnd := partStart
for keyEnd < partEnd && cc[keyEnd] != '=' {
keyEnd++
}

keyStart := partStart
for keyStart < keyEnd && (cc[keyStart] == ' ' || cc[keyStart] == '\t') {
keyStart++
}
for keyEnd > keyStart && (cc[keyEnd-1] == ' ' || cc[keyEnd-1] == '\t') {
keyEnd--
}

if keyStart < keyEnd && utils.EqualFold(cc[keyStart:keyEnd], directive) {
return true
}
}

start = end + 1
}

return false
Expand Down
29 changes: 29 additions & 0 deletions middleware/cache/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3046,6 +3046,35 @@ func Test_AllowsSharedCache(t *testing.T) {
})
}

func Test_HasDirective(t *testing.T) {
t.Parallel()

tests := []struct {
header string
directive string
expect bool
}{
{header: "no-cache", directive: "no-cache", expect: true},
{header: "NO-CACHE", directive: "no-cache", expect: true},
{header: "no-cache ", directive: "no-cache", expect: true},
{header: "no-cache\t", directive: "no-cache", expect: 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 test case for a trailing horizontal tab (\t) might fail with the current implementation of parseCacheControlDirectives. The parser (lines 952, 957, and 970 in cache.go) only trims the space character (' ') and does not account for tabs. Per RFC 9111, Optional White Space (OWS) consists of both spaces and horizontal tabs. To fully support OWS as intended by this PR, the underlying parser should be updated to handle both characters.

{header: "no-cache=\"Set-Cookie\"", directive: "no-cache", expect: true},
{header: "private ", directive: "private", expect: true},
{header: "public, private ", directive: "private", expect: true},
{header: "my-no-cache", directive: "no-cache", expect: false},
{header: "private-ish", directive: "private", expect: false},
}

for _, tt := range tests {
tt := tt
t.Run(tt.header+"_"+tt.directive, func(t *testing.T) {
t.Parallel()
got := hasDirective(tt.header, tt.directive)
require.Equal(t, tt.expect, got)
})
Comment on lines +3068 to +3074
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

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

Subtest names are built from the raw header value (tt.header+"_"+tt.directive). Because some cases include control characters (e.g. \t) and quotes, the test output and -run filtering can become hard to read/use. Consider using a sanitized name (e.g. fmt.Sprintf("%q_%s", tt.header, tt.directive) or an explicit name field).

Copilot uses AI. Check for mistakes.
}
}

func TestCacheSkipsAuthorizationByDefault(t *testing.T) {
t.Parallel()

Expand Down