From a61e6a7cd6f311e81170709fa48c83e60c245caf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 11:55:46 +0000 Subject: [PATCH 1/4] Initial plan From 5109bc4448d3c2f50cb2acbe105a8b1533ecf891 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 12:00:40 +0000 Subject: [PATCH 2/4] feat: support @dynamic=value syntax for per-response retry-after initial values Co-authored-by: waldekmastykarz <11164679+waldekmastykarz@users.noreply.github.com> --- .../Behavior/GenericRandomErrorPlugin.cs | 48 +++++++++++++------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/DevProxy.Plugins/Behavior/GenericRandomErrorPlugin.cs b/DevProxy.Plugins/Behavior/GenericRandomErrorPlugin.cs index 92ed9bc9..0f60e843 100644 --- a/DevProxy.Plugins/Behavior/GenericRandomErrorPlugin.cs +++ b/DevProxy.Plugins/Behavior/GenericRandomErrorPlugin.cs @@ -157,10 +157,10 @@ private void FailResponse(ProxyRequestArgs e) } } - private ThrottlingInfo ShouldThrottle(Request request, string throttlingKey) + private static ThrottlingInfo ShouldThrottle(Request request, string throttlingKey, int retryAfterInSeconds) { var throttleKeyForRequest = BuildThrottleKey(request); - return new(throttleKeyForRequest == throttlingKey ? Configuration.RetryAfterInSeconds : 0, "Retry-After"); + return new(throttleKeyForRequest == throttlingKey ? retryAfterInSeconds : 0, "Retry-After"); } private GenericErrorResponse? GetMatchingErrorResponse(Request request) @@ -222,21 +222,41 @@ private void UpdateProxyResponse(ProxyRequestArgs e, GenericErrorResponseRespons } if (error.StatusCode == (int)HttpStatusCode.TooManyRequests && - error.Headers is not null && - error.Headers.FirstOrDefault(h => h.Name is "Retry-After" or "retry-after")?.Value == "@dynamic") + error.Headers is not null) { - var retryAfterDate = DateTime.Now.AddSeconds(Configuration.RetryAfterInSeconds); - if (!e.GlobalData.TryGetValue(RetryAfterPlugin.ThrottledRequestsKey, out var value)) + var retryAfterHeader = error.Headers.FirstOrDefault(h => h.Name is "Retry-After" or "retry-after"); + if (retryAfterHeader?.Value is not null && retryAfterHeader.Value.StartsWith("@dynamic", StringComparison.OrdinalIgnoreCase)) { - value = new List(); - e.GlobalData.Add(RetryAfterPlugin.ThrottledRequestsKey, value); + // Parse @dynamic or @dynamic=value syntax + var retryAfterInSeconds = Configuration.RetryAfterInSeconds; + if (retryAfterHeader.Value.StartsWith("@dynamic=", StringComparison.OrdinalIgnoreCase)) + { + var valueStr = retryAfterHeader.Value.Substring("@dynamic=".Length); + if (int.TryParse(valueStr, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedValue)) + { + retryAfterInSeconds = parsedValue; + } + else + { + Logger.LogWarning("Invalid @dynamic value '{Value}' for Retry-After header. Using default value {Default}.", valueStr, Configuration.RetryAfterInSeconds); + } + } + + var retryAfterDate = DateTime.Now.AddSeconds(retryAfterInSeconds); + if (!e.GlobalData.TryGetValue(RetryAfterPlugin.ThrottledRequestsKey, out var value)) + { + value = new List(); + e.GlobalData.Add(RetryAfterPlugin.ThrottledRequestsKey, value); + } + var throttledRequests = value as List; + // Capture retryAfterInSeconds in the lambda to support per-response values + var capturedRetryAfter = retryAfterInSeconds; + throttledRequests?.Add(new(BuildThrottleKey(request), (req, key) => ShouldThrottle(req, key, capturedRetryAfter), 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"); + _ = headers.Remove(h); + headers.Add(new("Retry-After", retryAfterInSeconds.ToString(CultureInfo.InvariantCulture))); } - var throttledRequests = value as List; - throttledRequests?.Add(new(BuildThrottleKey(request), ShouldThrottle, 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"); - _ = headers.Remove(h); - headers.Add(new("Retry-After", Configuration.RetryAfterInSeconds.ToString(CultureInfo.InvariantCulture))); } var statusCode = (HttpStatusCode)(error.StatusCode ?? 400); From 0c1396533134ce9232beaa7a1000b12f77758c40 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 12:02:38 +0000 Subject: [PATCH 3/4] refactor: address code review feedback - use range syntax and simplify variable usage Co-authored-by: waldekmastykarz <11164679+waldekmastykarz@users.noreply.github.com> --- DevProxy.Plugins/Behavior/GenericRandomErrorPlugin.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/DevProxy.Plugins/Behavior/GenericRandomErrorPlugin.cs b/DevProxy.Plugins/Behavior/GenericRandomErrorPlugin.cs index 0f60e843..e3ba5d8b 100644 --- a/DevProxy.Plugins/Behavior/GenericRandomErrorPlugin.cs +++ b/DevProxy.Plugins/Behavior/GenericRandomErrorPlugin.cs @@ -231,7 +231,7 @@ private void UpdateProxyResponse(ProxyRequestArgs e, GenericErrorResponseRespons var retryAfterInSeconds = Configuration.RetryAfterInSeconds; if (retryAfterHeader.Value.StartsWith("@dynamic=", StringComparison.OrdinalIgnoreCase)) { - var valueStr = retryAfterHeader.Value.Substring("@dynamic=".Length); + var valueStr = retryAfterHeader.Value["@dynamic=".Length..]; if (int.TryParse(valueStr, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedValue)) { retryAfterInSeconds = parsedValue; @@ -249,9 +249,8 @@ private void UpdateProxyResponse(ProxyRequestArgs e, GenericErrorResponseRespons e.GlobalData.Add(RetryAfterPlugin.ThrottledRequestsKey, value); } var throttledRequests = value as List; - // Capture retryAfterInSeconds in the lambda to support per-response values - var capturedRetryAfter = retryAfterInSeconds; - throttledRequests?.Add(new(BuildThrottleKey(request), (req, key) => ShouldThrottle(req, key, capturedRetryAfter), retryAfterDate)); + 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"); _ = headers.Remove(h); From 6df0a530c5eae3b43c488f5e7c9ba8e93059e6cc Mon Sep 17 00:00:00 2001 From: Waldek Mastykarz Date: Mon, 19 Jan 2026 15:04:27 +0100 Subject: [PATCH 4/4] Update DevProxy.Plugins/Behavior/GenericRandomErrorPlugin.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- DevProxy.Plugins/Behavior/GenericRandomErrorPlugin.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/DevProxy.Plugins/Behavior/GenericRandomErrorPlugin.cs b/DevProxy.Plugins/Behavior/GenericRandomErrorPlugin.cs index e3ba5d8b..8a437734 100644 --- a/DevProxy.Plugins/Behavior/GenericRandomErrorPlugin.cs +++ b/DevProxy.Plugins/Behavior/GenericRandomErrorPlugin.cs @@ -234,7 +234,14 @@ private void UpdateProxyResponse(ProxyRequestArgs e, GenericErrorResponseRespons var valueStr = retryAfterHeader.Value["@dynamic=".Length..]; if (int.TryParse(valueStr, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedValue)) { - retryAfterInSeconds = parsedValue; + if (parsedValue < 0) + { + Logger.LogWarning("Negative @dynamic value '{Value}' for Retry-After header is not allowed. Using default value {Default}.", valueStr, Configuration.RetryAfterInSeconds); + } + else + { + retryAfterInSeconds = parsedValue; + } } else {