Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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.StatusCodeFilter == null || options.StatusCodeFilter.PassesFilter(cached.StatusCode))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

  • The PassesFilter(cached.StatusCode) method could serve as a small helper in the CacheOptions business object, though it depends on the final design.
  • The expression options.StatusCodeFilter.PassesFilter gives the impression that an additional filter has been injected into the logic, but that doesn’t seem to be the case. Since we’re not varying filters, we’ll just have a single filter.

{
_outputCache.Add(downStreamRequestCacheKey, cached, options.Region, ttl);
Logger.LogDebug(() => $"Finished response added to cache for the '{downstreamUrlKey}' key.");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please wrap the logging for the Debug scenario with a preprocessor directive, as another team member will likely request this 👇

Suggested change
Logger.LogDebug(() => $"Finished response added to cache for the '{downstreamUrlKey}' key.");
#if DEBUG
Logger.LogDebug(() => $"Finished response added to cache for the '{downstreamUrlKey}' key.");
#endif

}
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
15 changes: 12 additions & 3 deletions src/Ocelot/Configuration/CacheOptions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Ocelot.Configuration.File;
using Ocelot.Filter;
using Ocelot.Infrastructure.Extensions;
using Ocelot.Request.Middleware;

Expand All @@ -15,7 +16,11 @@ 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.StatusCodeFilter)
{ }

public CacheOptions(int ttlSeconds, string region, string header, bool? enableContentHashing)
: this(ttlSeconds, region, header, enableContentHashing, null)
{ }

/// <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="statusCodeFilter">The filter for discriminating what gets cached. <code>null</code> means "everything is whitelisted".</param>
public CacheOptions(int? ttlSeconds, string region, string header, bool? enableContentHashing, HttpStatusCodeFilter statusCodeFilter)
{
TtlSeconds = ttlSeconds ?? NoSeconds;
Region = region;
Header = header.IfEmpty(Oc_Cache_Control);
EnableContentHashing = enableContentHashing ?? false;
StatusCodeFilter = statusCodeFilter;
}

/// <summary>Time-to-live seconds.</summary>
Expand All @@ -53,4 +60,6 @@ public CacheOptions(int? ttlSeconds, string region, string header, bool? enableC
public bool EnableContentHashing { get; }

public bool UseCache => TtlSeconds > NoSeconds;

public HttpStatusCodeFilter StatusCodeFilter { get; }
}
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?.StatusCodeFilter);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It seems I missed this issue in the previous PR. Extending the argument list in a single constructor is not the right approach. It’s better to have a few overloaded constructors that cover most user scenarios.
Here’s what I propose:

Suggested change
=> new(options?.TtlSeconds, options?.Region, options?.Header, options?.EnableContentHashing, options?.StatusCodeFilter);
=> new(options ?? new());

The null-coalescing operator (??) is used to handle argument null checks within the new constructor which will be designed by you

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Can do. I was just following the existing pattern. Not my project, so did not figure it was my place to make that change.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Alright, never mind. I will handle it myself.


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.StatusCodeFilter ?? globalOptions.StatusCodeFilter;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

There's a lack of defaulting! Refer to my Idea 2 in the previous comment. We could implement defaulting closer to the Dev Complete stage, and I'll help with that.
Finally, I expect the following improvement:

Suggested change
var statusCodes = options.StatusCodeFilter ?? globalOptions.StatusCodeFilter;
var statusCodes = options.StatusCodes ?? globalOptions.StatusCodes ?? Ocelot.RecommendedStatusCodes;

return new CacheOptions(ttlSeconds, region, header, enableHashing, statusCodes);
}
}
7 changes: 6 additions & 1 deletion src/Ocelot/Configuration/File/FileCacheOptions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace Ocelot.Configuration.File;
using Ocelot.Filter;

namespace Ocelot.Configuration.File;

public class FileCacheOptions
{
Expand All @@ -10,6 +12,7 @@ public FileCacheOptions(FileCacheOptions from)
TtlSeconds = from.TtlSeconds;
Header = from.Header;
EnableContentHashing = from.EnableContentHashing;
StatusCodeFilter = from.StatusCodeFilter;
}

/// <summary>Using <see cref="Nullable{T}"/> where T is <see cref="int"/> to have <see langword="null"/> as default value and allowing global configuration usage.</summary>
Expand All @@ -23,4 +26,6 @@ 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 HttpStatusCodeFilter StatusCodeFilter { get; set; }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

$\color{red}{Critical\ issue\ with\ the\ property\ type!}$

Suggested change
public HttpStatusCodeFilter StatusCodeFilter { get; set; }
public int[] StatusCodes { get; set; }

Note that the type is not the old HttpStatusCode[] but rather int[], an array of integers. If an Ocelot user is not a C# developer, they might not be familiar with the HttpStatusCode enumeration and its values!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I've just pushed a new commit 9013937 with the fix, but I assume the HttpStatusCode enumeration is parsed without issues by the JSON deserializer from the System.Text.Json namespace. I am not so sure about the Newtonsoft.Json deserializer, which is currently the default parser in Ocelot, though we plan to replace Newtonsoft.Json with System.Text.Json soon. If Newtonsoft handles C# enumerations well, hopefully, it might be worth rethinking the proposed property. Either way, the int[] StatusCodes property will remain the main one. I just commented out the line so we can go over this again.

}
46 changes: 46 additions & 0 deletions src/Ocelot/Filter/Filter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Ocelot.Configuration.File;

namespace Ocelot.Filter
{
public abstract class Filter<T> : IFilter<T>
{
public FilterType FilterType { get; set; }
public T[] Values { get; set; }

public Filter() { }
public Filter(FilterType filterType, T[] values)
{
FilterType = filterType;
Values = values;
}

public virtual bool PassesFilter(T value)
{
// there's probably a clever way to clean this up but this is legible.
if (FilterType == FilterType.Blacklist)
{
foreach (var item in Values)
{
if (value.Equals(item))
{
return false; // the value is in the blacklist, so it can't pass the filter
}
}

return true; // the value wasn't found in the blacklist, so it passes the filter
}
else // filter is whitelist
{
foreach (var item in Values)
{
if (value.Equals(item))
{
return true; // the value is in the whitelist, so it passes the filter
}
}

return false; // the value is not in the whitelist, so it doesn't pass the filter
}
}
}
}
8 changes: 8 additions & 0 deletions src/Ocelot/Filter/FilterType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Ocelot.Filter
{
public enum FilterType
{
Whitelist,
Blacklist,
}
}
54 changes: 54 additions & 0 deletions src/Ocelot/Filter/HttpStatusCodeFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System;

namespace Ocelot.Filter
{
public class HttpStatusCodeFilter : Filter<HttpStatusCode>
Copy link
Copy Markdown
Member

@raman-m raman-m Nov 22, 2025

Choose a reason for hiding this comment

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

This filter will likely become useless because defining the values of the C# HttpStatusCode enumeration is challenging for Ocelot users who aren’t .NET developers. I’ve already raised this issue here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yes, I can change all this to be an integer based filter and not an HttpStatusCode enumeration based filter.

{
public HttpStatusCodeFilter() { }

public HttpStatusCodeFilter(FilterType filterType, HttpStatusCode[] values) : base(filterType, values ?? Enum.GetValues(typeof(HttpStatusCode)).Cast<HttpStatusCode>().ToArray())
{
}

public HttpStatusCodeFilter(FilterType filterType, string[] values) :base(filterType, ParseInput(values))
{
}

static HttpStatusCode[] ParseInput(string[] values)
{
List<HttpStatusCode> valuesList = new List<HttpStatusCode>();
if (values == null)
{
valuesList.AddRange(Enum.GetValues(typeof(HttpStatusCode)).Cast<HttpStatusCode>());
}
else
{
foreach (var value in values)
{
int i;
if (int.TryParse(value, out i))
{
if (Enum.IsDefined(typeof(HttpStatusCode), i))
{
valuesList.Add((HttpStatusCode)i);
}
}
else
{
foreach (var code in Enum.GetValues<HttpStatusCode>())
{
string codeClass = ((int)code / 100) + "xx";
bool code_is_in_range = string.Equals(value, codeClass, StringComparison.OrdinalIgnoreCase);
if (code_is_in_range)
{
valuesList.Add(code);
}
}
}
}
}

return valuesList.Distinct().ToArray();
}
}
}
7 changes: 7 additions & 0 deletions src/Ocelot/Filter/IFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Ocelot.Filter
{
public interface IFilter<T>
{
bool PassesFilter(T value);
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Are you suggesting using multiple filters for different options?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I thought perhaps having a generic Filter types could assist in the future. In particular, I saw #1587 and figured it could be used there, too. The type is unnecessary for the current feature though, so it can be removed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I thought perhaps having a generic Filter types could assist in the future.

Could assist in future?... Sorry? Are you a member of our team?

In particular, I saw #1587 and figured it could be used there, too.

No, it is better to focus on the feature you're currently working on rather than trying to enhance or refactor other features and code areas. I've requested community help for #1587 since the author has no intention of contributing, so they have been unassigned.

I think this filter might be useful for SecurityOptions, but I am not entirely sure.

}
Loading