diff --git a/docs/middleware/cache.md b/docs/middleware/cache.md index 0723c615dc..08c7ad5989 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 diff --git a/middleware/cache/cache.go b/middleware/cache/cache.go index 5c832f0b96..723b5321e2 100644 --- a/middleware/cache/cache.go +++ b/middleware/cache/cache.go @@ -48,6 +48,21 @@ var ignoreHeaders = map[string]any{ "Content-Encoding": nil, // already stored explicitly by the cache manager } +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 func New(config ...Config) fiber.Handler { // Set default config @@ -170,6 +185,12 @@ func New(config ...Config) fiber.Handler { return err } + // Don't cache response if status code is not cacheable + if !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..2193decb25 100644 --- a/middleware/cache/cache_test.go +++ b/middleware/cache/cache_test.go @@ -918,6 +918,87 @@ 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")) + require.NoError(t, 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.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()