From c4c58867afe275114ac2f232037ff078c73d72ba Mon Sep 17 00:00:00 2001 From: miyamo2 Date: Sat, 18 Jan 2025 17:12:09 +0900 Subject: [PATCH 1/7] =?UTF-8?q?=F0=9F=A9=B9=20Fix(v3;middleware/cache):=20?= =?UTF-8?q?don't=20cache=20if=20status=20code=20is=20not=20cacheable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- middleware/cache/cache.go | 13 ++++++ middleware/cache/cache_test.go | 84 ++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/middleware/cache/cache.go b/middleware/cache/cache.go index 5c832f0b96..803eb59a27 100644 --- a/middleware/cache/cache.go +++ b/middleware/cache/cache.go @@ -48,6 +48,13 @@ var ignoreHeaders = map[string]any{ "Content-Encoding": nil, // already stored explicitly by the cache manager } +var cacheableStatusCodes = []int{ + fiber.StatusOK, fiber.StatusNonAuthoritativeInformation, fiber.StatusNoContent, fiber.StatusPartialContent, + fiber.StatusMultipleChoices, fiber.StatusMovedPermanently, + fiber.StatusNotFound, fiber.StatusMethodNotAllowed, fiber.StatusGone, fiber.StatusRequestURITooLong, + fiber.StatusNotImplemented, +} + // New creates a new middleware handler func New(config ...Config) fiber.Handler { // Set default config @@ -170,6 +177,12 @@ func New(config ...Config) fiber.Handler { return err } + // Don't cache response if status code is not cacheable + if !slices.Contains(cacheableStatusCodes, c.Response().StatusCode()) { + c.Set(cfg.CacheHeader, cacheUnreachable) + return nil + } + // lock entry back and unlock on finish mux.Lock() defer mux.Unlock() diff --git a/middleware/cache/cache_test.go b/middleware/cache/cache_test.go index 22ab0e2895..326ac9f564 100644 --- a/middleware/cache/cache_test.go +++ b/middleware/cache/cache_test.go @@ -918,6 +918,90 @@ func Test_Cache_MaxBytesSizes(t *testing.T) { } } +func Test_Cache_UncacheableStatusCodes(t *testing.T) { + t.Parallel() + app := fiber.New() + app.Use(New()) + + app.Get("/:statusCode", func(c fiber.Ctx) error { + statusCode, err := strconv.Atoi(c.Params("statusCode")) + if err != nil { + return err + } + return c.Status(statusCode).SendString("foo") + }) + + uncacheableStatusCodes := []int{ + // Informational responses + fiber.StatusContinue, + fiber.StatusSwitchingProtocols, + fiber.StatusProcessing, + fiber.StatusEarlyHints, + + // Successful responses + fiber.StatusCreated, + fiber.StatusAccepted, + fiber.StatusResetContent, + fiber.StatusMultiStatus, + fiber.StatusAlreadyReported, + fiber.StatusIMUsed, + + // Redirection responses + fiber.StatusFound, + fiber.StatusSeeOther, + fiber.StatusNotModified, + fiber.StatusUseProxy, + fiber.StatusSwitchProxy, + fiber.StatusTemporaryRedirect, + fiber.StatusPermanentRedirect, + + // Client error responses + fiber.StatusBadRequest, + fiber.StatusUnauthorized, + fiber.StatusPaymentRequired, + fiber.StatusForbidden, + fiber.StatusNotAcceptable, + fiber.StatusProxyAuthRequired, + fiber.StatusRequestTimeout, + fiber.StatusConflict, + fiber.StatusLengthRequired, + fiber.StatusPreconditionFailed, + fiber.StatusRequestEntityTooLarge, + fiber.StatusUnsupportedMediaType, + fiber.StatusRequestedRangeNotSatisfiable, + fiber.StatusExpectationFailed, + fiber.StatusTeapot, + fiber.StatusMisdirectedRequest, + fiber.StatusUnprocessableEntity, + fiber.StatusLocked, + fiber.StatusFailedDependency, + fiber.StatusTooEarly, + fiber.StatusUpgradeRequired, + fiber.StatusPreconditionRequired, + fiber.StatusTooManyRequests, + fiber.StatusRequestHeaderFieldsTooLarge, + fiber.StatusUnavailableForLegalReasons, + + // Server error responses + fiber.StatusInternalServerError, + fiber.StatusBadGateway, + fiber.StatusServiceUnavailable, + fiber.StatusGatewayTimeout, + fiber.StatusHTTPVersionNotSupported, + fiber.StatusVariantAlsoNegotiates, + fiber.StatusInsufficientStorage, + fiber.StatusLoopDetected, + fiber.StatusNotExtended, + fiber.StatusNetworkAuthenticationRequired, + } + for _, v := range uncacheableStatusCodes { + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, fmt.Sprintf("/%d", v), nil)) + require.NoError(t, err) + require.Equal(t, cacheUnreachable, resp.Header.Get("X-Cache")) + require.Equal(t, v, resp.StatusCode) + } +} + // go test -v -run=^$ -bench=Benchmark_Cache -benchmem -count=4 func Benchmark_Cache(b *testing.B) { app := fiber.New() From a6c66a74ebcbdf4e8450eaa71340e29b9f5302a2 Mon Sep 17 00:00:00 2001 From: miyamo2 Date: Sat, 18 Jan 2025 17:58:22 +0900 Subject: [PATCH 2/7] allow 418 TeaPot --- middleware/cache/cache.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/middleware/cache/cache.go b/middleware/cache/cache.go index 803eb59a27..ba38443603 100644 --- a/middleware/cache/cache.go +++ b/middleware/cache/cache.go @@ -51,7 +51,7 @@ var ignoreHeaders = map[string]any{ var cacheableStatusCodes = []int{ fiber.StatusOK, fiber.StatusNonAuthoritativeInformation, fiber.StatusNoContent, fiber.StatusPartialContent, fiber.StatusMultipleChoices, fiber.StatusMovedPermanently, - fiber.StatusNotFound, fiber.StatusMethodNotAllowed, fiber.StatusGone, fiber.StatusRequestURITooLong, + fiber.StatusNotFound, fiber.StatusMethodNotAllowed, fiber.StatusGone, fiber.StatusTeapot, fiber.StatusRequestURITooLong, fiber.StatusNotImplemented, } From 86f9577306924dc2c000950cd32639a23ac3e193 Mon Sep 17 00:00:00 2001 From: miyamo2 Date: Sat, 18 Jan 2025 18:03:46 +0900 Subject: [PATCH 3/7] fix test --- middleware/cache/cache_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/middleware/cache/cache_test.go b/middleware/cache/cache_test.go index 326ac9f564..76c83571d0 100644 --- a/middleware/cache/cache_test.go +++ b/middleware/cache/cache_test.go @@ -970,7 +970,6 @@ func Test_Cache_UncacheableStatusCodes(t *testing.T) { fiber.StatusUnsupportedMediaType, fiber.StatusRequestedRangeNotSatisfiable, fiber.StatusExpectationFailed, - fiber.StatusTeapot, fiber.StatusMisdirectedRequest, fiber.StatusUnprocessableEntity, fiber.StatusLocked, From 45a7b06052c3aeef7e6f0d6f18425b26476e39a1 Mon Sep 17 00:00:00 2001 From: miyamo2 Date: Sat, 18 Jan 2025 18:25:36 +0900 Subject: [PATCH 4/7] fix lint error --- middleware/cache/cache_test.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/middleware/cache/cache_test.go b/middleware/cache/cache_test.go index 76c83571d0..2193decb25 100644 --- a/middleware/cache/cache_test.go +++ b/middleware/cache/cache_test.go @@ -925,9 +925,7 @@ func Test_Cache_UncacheableStatusCodes(t *testing.T) { app.Get("/:statusCode", func(c fiber.Ctx) error { statusCode, err := strconv.Atoi(c.Params("statusCode")) - if err != nil { - return err - } + require.NoError(t, err) return c.Status(statusCode).SendString("foo") }) From 04eb05e98b21b7a7b93448beceec3c28b0e45b3a Mon Sep 17 00:00:00 2001 From: miyamo2 Date: Sun, 19 Jan 2025 00:55:18 +0900 Subject: [PATCH 5/7] check cacheability with map --- middleware/cache/cache.go | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/middleware/cache/cache.go b/middleware/cache/cache.go index ba38443603..723b5321e2 100644 --- a/middleware/cache/cache.go +++ b/middleware/cache/cache.go @@ -48,11 +48,19 @@ var ignoreHeaders = map[string]any{ "Content-Encoding": nil, // already stored explicitly by the cache manager } -var cacheableStatusCodes = []int{ - fiber.StatusOK, fiber.StatusNonAuthoritativeInformation, fiber.StatusNoContent, fiber.StatusPartialContent, - fiber.StatusMultipleChoices, fiber.StatusMovedPermanently, - fiber.StatusNotFound, fiber.StatusMethodNotAllowed, fiber.StatusGone, fiber.StatusTeapot, fiber.StatusRequestURITooLong, - fiber.StatusNotImplemented, +var cacheableStatusCodes = map[int]bool{ + fiber.StatusOK: true, + fiber.StatusNonAuthoritativeInformation: true, + fiber.StatusNoContent: true, + fiber.StatusPartialContent: true, + fiber.StatusMultipleChoices: true, + fiber.StatusMovedPermanently: true, + fiber.StatusNotFound: true, + fiber.StatusMethodNotAllowed: true, + fiber.StatusGone: true, + fiber.StatusRequestURITooLong: true, + fiber.StatusTeapot: true, + fiber.StatusNotImplemented: true, } // New creates a new middleware handler @@ -178,7 +186,7 @@ func New(config ...Config) fiber.Handler { } // Don't cache response if status code is not cacheable - if !slices.Contains(cacheableStatusCodes, c.Response().StatusCode()) { + if !cacheableStatusCodes[c.Response().StatusCode()] { c.Set(cfg.CacheHeader, cacheUnreachable) return nil } From 899d82d28b7725e2fe6b7aafd5f18b1fe54bf417 Mon Sep 17 00:00:00 2001 From: miyamo2 Date: Mon, 20 Jan 2025 05:47:07 +0900 Subject: [PATCH 6/7] documentation --- docs/middleware/cache.md | 25 +++++++++++++++++++++++++ docs/whats_new.md | 3 ++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/docs/middleware/cache.md b/docs/middleware/cache.md index 0723c615dc..9d0d21af52 100644 --- a/docs/middleware/cache.md +++ b/docs/middleware/cache.md @@ -10,6 +10,31 @@ Request Directives
`Cache-Control: no-cache` will return the up-to-date response but still caches it. You will always get a `miss` cache status.
`Cache-Control: no-store` will refrain from caching. You will always get the up-to-date response. +Cacheable Status Codes
+ +This middleware caches responses with the following status codes according to RFC7231: + +- `200: OK` +- `203: Non-Authoritative Information` +- `204: No Content` +- `206: Partial Content` +- `300: Multiple Choices` +- `301: Moved Permanently` +- `404: Not Found` +- `405: Method Not Allowed` +- `410: Gone` +- `414: URI Too Long` +- `501: Not Implemented` + +Additionally, `418: I'm a teapot` is not originally cacheable but is cached by this middleware. +If the status code is other than these, you will always get an `unreachable` cache status. + +For more information about cacheable status codes or RFC7231, please refer to the following resources: + +- [Cacheable - MDN Web Docs](https://developer.mozilla.org/en-US/docs/Glossary/Cacheable) + +- [RFC7231 - Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content](https://datatracker.ietf.org/doc/html/rfc7231) + ## Signatures ```go diff --git a/docs/whats_new.md b/docs/whats_new.md index 81339d6a48..bb9870ddb6 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -733,7 +733,8 @@ The adaptor middleware has been significantly optimized for performance and effi ### Cache -We are excited to introduce a new option in our caching middleware: Cache Invalidator. This feature provides greater control over cache management, allowing you to define a custom conditions for invalidating cache entries. +We are excited to introduce a new option in our caching middleware: Cache Invalidator. This feature provides greater control over cache management, allowing you to define a custom conditions for invalidating cache entries. +Additionally, the caching middleware has been optimized to avoid caching non-cacheable status codes, as defined by the [HTTP standards](https://datatracker.ietf.org/doc/html/rfc7231#section-6.1). This improvement enhances cache accuracy and reduces unnecessary cache storage usage. ### CORS From fef2ba278ca7f3ce061a9b9b6d6f3803c4d2e1d5 Mon Sep 17 00:00:00 2001 From: miyamo2 Date: Mon, 20 Jan 2025 05:51:16 +0900 Subject: [PATCH 7/7] fix: markdown lint --- docs/middleware/cache.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/middleware/cache.md b/docs/middleware/cache.md index 9d0d21af52..08c7ad5989 100644 --- a/docs/middleware/cache.md +++ b/docs/middleware/cache.md @@ -27,7 +27,7 @@ This middleware caches responses with the following status codes according to RF - `501: Not Implemented` Additionally, `418: I'm a teapot` is not originally cacheable but is cached by this middleware. -If the status code is other than these, you will always get an `unreachable` cache status. +If the status code is other than these, you will always get an `unreachable` cache status. For more information about cacheable status codes or RFC7231, please refer to the following resources: