Skip to content
Open
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
12 changes: 10 additions & 2 deletions src/Ocelot/Cache/OutputCacheMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
33 changes: 30 additions & 3 deletions src/Ocelot/Configuration/CacheOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{ }


/// <summary>
/// Initializes a new instance of the <see cref="CacheOptions"/> class.
/// </summary>
Expand All @@ -31,13 +36,15 @@ public CacheOptions(FileCacheOptions from, string defaultRegion)
/// <param name="ttlSeconds">Time-to-live seconds. If not speciefied, zero value is used by default.</param>
/// <param name="region">The region of caching.</param>
/// <param name="header">The header name to control cached value.</param>
/// <param name="enableContentHashing">The switcher for content hashing. If not speciefied, false value is used by default.</param>
public CacheOptions(int? ttlSeconds, string region, string header, bool? enableContentHashing)
/// <param name="enableContentHashing">The switcher for content hashing. If not speciefied, false value is used by default.</param>
/// <param name="statusCodes">Status codes to filter.</param>
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);
}

/// <summary>Time-to-live seconds.</summary>
Expand All @@ -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();
}
}
5 changes: 3 additions & 2 deletions src/Ocelot/Configuration/Creator/CacheOptionsCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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);
}
}
3 changes: 3 additions & 0 deletions src/Ocelot/Configuration/File/FileCacheOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,7 @@ public FileCacheOptions(FileCacheOptions from)
/// <remarks>If <see langword="null"/> then use global configuration with <see langword="false"/> by default.</remarks>
/// <value><see langword="true"/> if content hashing is enabled; otherwise, <see langword="false"/>.</value>
public bool? EnableContentHashing { get; set; }

//public HttpStatusCode[] StatusCodes { get; set; }
public int[] StatusCodes { get; set; }
}
64 changes: 64 additions & 0 deletions test/Ocelot.AcceptanceTests/Caching/CachingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
38 changes: 38 additions & 0 deletions test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading