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
47 changes: 47 additions & 0 deletions DevProxy.Integration.Tests/BehaviorPluginsIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,53 @@ public async Task GenericRandomError_Rate100_OverridesOriginWithConfiguredError(
Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
}

[Fact]
public async Task GenericRandomError_DynamicRetryAfterOnNon429_IsResolved()
{
await using var origin = await FakeOrigin.StartAsync();
var urls = KestrelProxyHarness.BuildUrlsToWatch(origin.Host);

var config = PluginConfig.FromJson($$"""
{
"rate": 100,
"errors": [
{
"request": { "url": "http://{{origin.Host}}/*" },
"responses": [
{
"statusCode": 503,
"headers": [ { "name": "RETRY-after", "value": "@dynamic=7" } ]
}
]
}
]
}
""");
var plugin = new GenericRandomErrorPlugin(
SharedHttpClient,
NullLogger<GenericRandomErrorPlugin>.Instance,
urls,
ProxyConfig,
config);
var retryAfter = new RetryAfterPlugin(
NullLogger<RetryAfterPlugin>.Instance,
urls);

await using var proxy = await KestrelProxyHarness.StartAsync(
origin.Host, [retryAfter, plugin]);
using var client = proxy.CreateHttpClient();

using var response = await client.GetAsync(new Uri($"http://{origin.Host}/get"));
// an immediate retry must be throttled by RetryAfterPlugin, proving the
// non-429 error registered the request for throttling
using var retry = await client.GetAsync(new Uri($"http://{origin.Host}/get"));

Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
Assert.True(response.Headers.TryGetValues("Retry-After", out var values));
Assert.Equal("7", Assert.Single(values));
Assert.Equal(HttpStatusCode.TooManyRequests, retry.StatusCode);
}

[Fact]
public async Task Latency_AddsConfiguredDelayBeforeForwarding()
{
Expand Down
9 changes: 5 additions & 4 deletions DevProxy.Plugins/Behavior/GenericRandomErrorPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -219,10 +219,11 @@ private void UpdateProxyResponse(ProxyRequestArgs e, GenericErrorResponseRespons
headers.AddRange(error.Headers);
}

if (error.StatusCode == (int)HttpStatusCode.TooManyRequests &&
error.Headers is not null)
// Retry-After can be sent with any status code (eg. 429, 503, 529),
// so resolve @dynamic regardless of the status code
if (error.Headers is not null)
{
var retryAfterHeader = error.Headers.FirstOrDefault(h => h.Name is "Retry-After" or "retry-after");
var retryAfterHeader = error.Headers.FirstOrDefault(h => string.Equals(h.Name, "Retry-After", StringComparison.OrdinalIgnoreCase));
if (retryAfterHeader?.Value is not null && retryAfterHeader.Value.StartsWith("@dynamic", StringComparison.OrdinalIgnoreCase))
{
// Parse @dynamic or @dynamic=value syntax
Expand Down Expand Up @@ -257,7 +258,7 @@ private void UpdateProxyResponse(ProxyRequestArgs e, GenericErrorResponseRespons
var throttleKey = BuildThrottleKey(request);
throttledRequests?.Add(new(throttleKey, (req, key) => ShouldThrottle(req, key, retryAfterInSeconds), retryAfterDate));
// replace the header with the @dynamic value with the actual value
var h = headers.First(h => h.Name is "Retry-After" or "retry-after");
var h = headers.First(h => string.Equals(h.Name, "Retry-After", StringComparison.OrdinalIgnoreCase));
_ = headers.Remove(h);
headers.Add(new("Retry-After", retryAfterInSeconds.ToString(CultureInfo.InvariantCulture)));
}
Expand Down
Loading