diff --git a/src/Ocelot/Cache/OutputCacheMiddleware.cs b/src/Ocelot/Cache/OutputCacheMiddleware.cs
index 4eb02ae08..ca130abe2 100644
--- a/src/Ocelot/Cache/OutputCacheMiddleware.cs
+++ b/src/Ocelot/Cache/OutputCacheMiddleware.cs
@@ -60,8 +60,16 @@ public async Task Invoke(HttpContext httpContext)
cached = await CreateCachedResponse(downstreamResponse);
var ttl = TimeSpan.FromSeconds(options.TtlSeconds);
- _outputCache.Add(downStreamRequestCacheKey, cached, options.Region, ttl);
- Logger.LogDebug(() => $"Finished response added to cache for the '{downstreamUrlKey}' key.");
+ if (options.StatusCodes == null || options.StatusCodes.Length == 0 || options.StatusCodes.Contains(cached.StatusCode))
+ {
+ _outputCache.Add(downStreamRequestCacheKey, cached, options.Region, ttl);
+ Logger.LogDebug(() => $"Finished response added to cache for the '{downstreamUrlKey}' key.");
+ }
+ else
+ {
+ Logger.LogDebug(() => $"Finished response filtered out from being added to cache due to response status for the '{downstreamUrlKey}' key.");
+ }
+
}
private static void SetHttpResponseMessageThisRequest(HttpContext context, DownstreamResponse response)
diff --git a/src/Ocelot/Configuration/CacheOptions.cs b/src/Ocelot/Configuration/CacheOptions.cs
index a3b77d0a6..adf43216b 100644
--- a/src/Ocelot/Configuration/CacheOptions.cs
+++ b/src/Ocelot/Configuration/CacheOptions.cs
@@ -15,9 +15,14 @@ public class CacheOptions
internal CacheOptions() { }
public CacheOptions(FileCacheOptions from, string defaultRegion)
- : this(from.TtlSeconds, from.Region.IfEmpty(defaultRegion), from.Header, from.EnableContentHashing)
+ : this(from.TtlSeconds, from.Region.IfEmpty(defaultRegion), from.Header, from.EnableContentHashing, from.StatusCodes)
{ }
+ public CacheOptions(int ttlSeconds, string region, string header, bool? enableContentHashing)
+ : this(ttlSeconds, region, header, enableContentHashing, null)
+ { }
+
+
///
/// Initializes a new instance of the class.
///
@@ -31,13 +36,15 @@ public CacheOptions(FileCacheOptions from, string defaultRegion)
/// Time-to-live seconds. If not speciefied, zero value is used by default.
/// The region of caching.
/// The header name to control cached value.
- /// The switcher for content hashing. If not speciefied, false value is used by default.
- public CacheOptions(int? ttlSeconds, string region, string header, bool? enableContentHashing)
+ /// The switcher for content hashing. If not speciefied, false value is used by default.
+ /// Status codes to filter.
+ public CacheOptions(int? ttlSeconds, string region, string header, bool? enableContentHashing, int[] statusCodes)
{
TtlSeconds = ttlSeconds ?? NoSeconds;
Region = region;
Header = header.IfEmpty(Oc_Cache_Control);
EnableContentHashing = enableContentHashing ?? false;
+ StatusCodes = ConvertToHttpStatusCodes(statusCodes);
}
/// Time-to-live seconds.
@@ -53,4 +60,24 @@ public CacheOptions(int? ttlSeconds, string region, string header, bool? enableC
public bool EnableContentHashing { get; }
public bool UseCache => TtlSeconds > NoSeconds;
+
+ public HttpStatusCode[] StatusCodes { get; }
+
+ private static HttpStatusCode[] ConvertToHttpStatusCodes(int[] statusCodes)
+ {
+ if (statusCodes is null || statusCodes.Length == 0)
+ {
+ return [];
+ }
+
+ return statusCodes.Select(code =>
+ {
+ if (!Enum.IsDefined(typeof(HttpStatusCode), code))
+ {
+ throw new ArgumentException($"Invalid HTTP status code: {code}", nameof(statusCodes));
+ }
+
+ return (HttpStatusCode)code;
+ }).ToArray();
+ }
}
diff --git a/src/Ocelot/Configuration/Creator/CacheOptionsCreator.cs b/src/Ocelot/Configuration/Creator/CacheOptionsCreator.cs
index 0d11eb058..4c12cc1e7 100644
--- a/src/Ocelot/Configuration/Creator/CacheOptionsCreator.cs
+++ b/src/Ocelot/Configuration/Creator/CacheOptionsCreator.cs
@@ -6,7 +6,7 @@ namespace Ocelot.Configuration.Creator;
public class CacheOptionsCreator : ICacheOptionsCreator
{
public CacheOptions Create(FileCacheOptions options)
- => new(options?.TtlSeconds, options?.Region, options?.Header, options?.EnableContentHashing);
+ => new(options?.TtlSeconds, options?.Region, options?.Header, options?.EnableContentHashing, options?.StatusCodes);
public CacheOptions Create(FileRoute route, FileGlobalConfiguration globalConfiguration, string loadBalancingKey)
{
@@ -58,6 +58,7 @@ protected virtual CacheOptions Merge(FileCacheOptions options, FileCacheOptions
var header = options.Header.IfEmpty(globalOptions.Header).IfEmpty(CacheOptions.Oc_Cache_Control);
var ttlSeconds = options.TtlSeconds ?? globalOptions.TtlSeconds;
var enableHashing = options.EnableContentHashing ?? globalOptions.EnableContentHashing;
- return new CacheOptions(ttlSeconds, region, header, enableHashing);
+ var statusCodes = options.StatusCodes ?? globalOptions.StatusCodes;
+ return new CacheOptions(ttlSeconds, region, header, enableHashing, statusCodes);
}
}
diff --git a/src/Ocelot/Configuration/File/FileCacheOptions.cs b/src/Ocelot/Configuration/File/FileCacheOptions.cs
index 81a01d943..7a2fb5b55 100644
--- a/src/Ocelot/Configuration/File/FileCacheOptions.cs
+++ b/src/Ocelot/Configuration/File/FileCacheOptions.cs
@@ -23,4 +23,7 @@ public FileCacheOptions(FileCacheOptions from)
/// If then use global configuration with by default.
/// if content hashing is enabled; otherwise, .
public bool? EnableContentHashing { get; set; }
+
+ //public HttpStatusCode[] StatusCodes { get; set; }
+ public int[] StatusCodes { get; set; }
}
diff --git a/test/Ocelot.AcceptanceTests/Caching/CachingTests.cs b/test/Ocelot.AcceptanceTests/Caching/CachingTests.cs
index b774c90cb..f6a06e3cf 100644
--- a/test/Ocelot.AcceptanceTests/Caching/CachingTests.cs
+++ b/test/Ocelot.AcceptanceTests/Caching/CachingTests.cs
@@ -215,6 +215,70 @@ public void Should_return_same_cached_response_when_request_body_changes_and_Ena
.BDDfy();
}
+ [Theory]
+ [InlineData(null, HttpStatusCode.OK)]
+ [InlineData(null, HttpStatusCode.Forbidden)]
+ [InlineData(null, HttpStatusCode.InternalServerError)]
+ [InlineData(null, HttpStatusCode.Unauthorized)]
+ [InlineData(new int[] { (int)HttpStatusCode.OK, (int)HttpStatusCode.Forbidden }, HttpStatusCode.OK)]
+ [InlineData(new int[] { StatusCodes.Status200OK, StatusCodes.Status403Forbidden }, HttpStatusCode.Forbidden)]
+ [Trait("Feat", "741")]
+ public void Should_cache_when_whitelisted(int[] statusCodes, HttpStatusCode responseCode)
+ {
+ // Arrange
+ var port = PortFinder.GetRandomPort();
+ var options = new FileCacheOptions
+ {
+ TtlSeconds = 100,
+ StatusCodes = statusCodes,
+ };
+ var (testBody1String, testBody2String) = TestBodiesFactory();
+ var configuration = GivenFileConfiguration(port, options);
+
+ this.Given(x => x.GivenThereIsAServiceRunningOn(port, responseCode, HelloLauraContent, null, null))
+ .And(x => GivenThereIsAConfiguration(configuration))
+ .And(x => GivenOcelotIsRunning())
+ .When(x => WhenIGetUrlOnTheApiGateway("/"))
+ .Then(x => ThenTheStatusCodeShouldBe(responseCode))
+ .And(x => ThenTheResponseBodyShouldBe(HelloLauraContent))
+ .Given(x => x.GivenTheServiceNowReturns(port, responseCode, HelloTomContent, null, null))
+ .When(x => WhenIGetUrlOnTheApiGateway("/"))
+ .Then(x => ThenTheStatusCodeShouldBe(responseCode))
+ .And(x => ThenTheResponseBodyShouldBe(HelloLauraContent))
+ .And(x => ThenTheContentLengthIs(HelloLauraContent.Length))
+ .BDDfy();
+ }
+
+ [Theory]
+ [InlineData(new int[] { StatusCodes.Status200OK, StatusCodes.Status403Forbidden }, HttpStatusCode.InternalServerError)]
+ [InlineData(new int[] { StatusCodes.Status200OK, StatusCodes.Status403Forbidden }, HttpStatusCode.BadRequest)]
+ [Trait("Feat", "741")]
+ public void Should_not_cache_when_not_whitelisted(int[] statusCodes, HttpStatusCode responseCode)
+ {
+ // Arrange
+ var port = PortFinder.GetRandomPort();
+ var options = new FileCacheOptions
+ {
+ TtlSeconds = 100,
+ StatusCodes = statusCodes,
+ };
+ var (testBody1String, testBody2String) = TestBodiesFactory();
+ var configuration = GivenFileConfiguration(port, options);
+
+ this.Given(x => x.GivenThereIsAServiceRunningOn(port, responseCode, HelloLauraContent, null, null))
+ .And(x => GivenThereIsAConfiguration(configuration))
+ .And(x => GivenOcelotIsRunning())
+ .When(x => WhenIGetUrlOnTheApiGateway("/"))
+ .Then(x => ThenTheStatusCodeShouldBe(responseCode))
+ .And(x => ThenTheResponseBodyShouldBe(HelloLauraContent))
+ .Given(x => x.GivenTheServiceNowReturns(port, responseCode, HelloTomContent, null, null))
+ .When(x => WhenIGetUrlOnTheApiGateway("/"))
+ .Then(x => ThenTheStatusCodeShouldBe(responseCode))
+ .And(x => ThenTheResponseBodyShouldBe(HelloTomContent))
+ .And(x => ThenTheContentLengthIs(HelloTomContent.Length))
+ .BDDfy();
+ }
+
[Fact]
[Trait("Issue", "1172")]
public void Should_clean_cached_response_by_cache_header_via_new_caching_key()
diff --git a/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs b/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs
index 97826bc0e..3ccb1d00d 100644
--- a/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs
+++ b/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs
@@ -150,6 +150,44 @@ private void ThenTheMessageIs(string expected)
Assert.Equal(expected, msg);
}
+ [Theory]
+ [InlineData(null, HttpStatusCode.OK)]
+ [InlineData(null, HttpStatusCode.Forbidden)]
+ [InlineData(null, HttpStatusCode.InternalServerError)]
+ [InlineData(null, HttpStatusCode.Unauthorized)]
+ [InlineData(new int[] { StatusCodes.Status200OK, StatusCodes.Status403Forbidden }, HttpStatusCode.OK)]
+ [InlineData(new int[] { StatusCodes.Status200OK, StatusCodes.Status403Forbidden }, HttpStatusCode.Forbidden)]
+ public async Task Should_cache_when_whitelisted(int[] statusCodes, HttpStatusCode responseCode)
+ {
+ // Arrange
+ var response = new HttpResponseMessage(responseCode);
+ GivenResponseIsNotCached(response);
+ GivenTheDownstreamRouteIs(new CacheOptions(100, "kanken", null, false, statusCodes));
+
+ // Act
+ await WhenICallTheMiddlewareAsync();
+
+ // Assert
+ ThenTheCacheAddIsCalled(Times.Once);
+ }
+
+ [Theory]
+ [InlineData(new int[] { StatusCodes.Status200OK, StatusCodes.Status403Forbidden }, HttpStatusCode.InternalServerError)]
+ [InlineData(new int[] { StatusCodes.Status200OK, StatusCodes.Status403Forbidden }, HttpStatusCode.BadRequest)]
+ public async Task Should_not_cache_when_not_whitelisted(int[] statusCodes, HttpStatusCode responseCode)
+ {
+ // Arrange
+ var response = new HttpResponseMessage(responseCode);
+ GivenResponseIsNotCached(response);
+ GivenTheDownstreamRouteIs(new CacheOptions(100, "kanken", null, false, statusCodes));
+
+ // Act
+ await WhenICallTheMiddlewareAsync();
+
+ // Assert
+ ThenTheCacheAddIsCalled(Times.Never);
+ }
+
private async Task WhenICallTheMiddlewareAsync()
{
_middleware = new OutputCacheMiddleware(_next, _loggerFactory.Object, _cache.Object, _cacheGenerator.Object);