Skip to content

Commit 6ff9060

Browse files
authored
Merge branch 'main' into llm-rate-limit-error-body
2 parents 458b449 + e9d0649 commit 6ff9060

8 files changed

Lines changed: 84 additions & 30 deletions

File tree

‎DevProxy.Abstractions/DevProxy.Abstractions.csproj‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.12" />
4242
<PackageReference Include="Microsoft.OpenApi" Version="3.10.2" />
4343
<PackageReference Include="Microsoft.OpenApi.YamlReader" Version="3.10.2" />
44-
<PackageReference Include="Newtonsoft.Json.Schema" Version="4.0.1" />
44+
<PackageReference Include="Newtonsoft.Json.Schema" Version="4.0.2" />
4545
<PackageReference Include="Scriban" Version="7.5.0" />
4646
<PackageReference Include="System.CommandLine" Version="2.0.12" />
4747
<PackageReference Include="YamlDotNet" Version="18.1.0" />

‎DevProxy.Abstractions/packages.lock.json‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,11 @@
8888
},
8989
"Newtonsoft.Json.Schema": {
9090
"type": "Direct",
91-
"requested": "[4.0.1, )",
92-
"resolved": "4.0.1",
93-
"contentHash": "rbHUKp5WTIbqmLEeJ21nTTDGcfR0LA7bVMzm0bYc3yx6NFKiCIHzzvYbwA4Sqgs7+wNldc5nBlkbithWj8IZig==",
91+
"requested": "[4.0.2, )",
92+
"resolved": "4.0.2",
93+
"contentHash": "+Kyhf67t35xoUycpSkaaxGeBaTLz/sZOPEW94HCVM/WJFVJooIsIHGUV7hS8xSvT1Ci78XwN4Fr57927OGYE6w==",
9494
"dependencies": {
95-
"Newtonsoft.Json": "13.0.3"
95+
"Newtonsoft.Json": "13.0.4"
9696
}
9797
},
9898
"Scriban": {
@@ -269,8 +269,8 @@
269269
},
270270
"Newtonsoft.Json": {
271271
"type": "Transitive",
272-
"resolved": "13.0.3",
273-
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
272+
"resolved": "13.0.4",
273+
"contentHash": "pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A=="
274274
},
275275
"SharpYaml": {
276276
"type": "Transitive",

‎DevProxy.Integration.Tests/BehaviorPluginsIntegrationTests.cs‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,53 @@ public async Task GenericRandomError_Rate100_OverridesOriginWithConfiguredError(
6666
Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
6767
}
6868

69+
[Fact]
70+
public async Task GenericRandomError_DynamicRetryAfterOnNon429_IsResolved()
71+
{
72+
await using var origin = await FakeOrigin.StartAsync();
73+
var urls = KestrelProxyHarness.BuildUrlsToWatch(origin.Host);
74+
75+
var config = PluginConfig.FromJson($$"""
76+
{
77+
"rate": 100,
78+
"errors": [
79+
{
80+
"request": { "url": "http://{{origin.Host}}/*" },
81+
"responses": [
82+
{
83+
"statusCode": 503,
84+
"headers": [ { "name": "RETRY-after", "value": "@dynamic=7" } ]
85+
}
86+
]
87+
}
88+
]
89+
}
90+
""");
91+
var plugin = new GenericRandomErrorPlugin(
92+
SharedHttpClient,
93+
NullLogger<GenericRandomErrorPlugin>.Instance,
94+
urls,
95+
ProxyConfig,
96+
config);
97+
var retryAfter = new RetryAfterPlugin(
98+
NullLogger<RetryAfterPlugin>.Instance,
99+
urls);
100+
101+
await using var proxy = await KestrelProxyHarness.StartAsync(
102+
origin.Host, [retryAfter, plugin]);
103+
using var client = proxy.CreateHttpClient();
104+
105+
using var response = await client.GetAsync(new Uri($"http://{origin.Host}/get"));
106+
// an immediate retry must be throttled by RetryAfterPlugin, proving the
107+
// non-429 error registered the request for throttling
108+
using var retry = await client.GetAsync(new Uri($"http://{origin.Host}/get"));
109+
110+
Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
111+
Assert.True(response.Headers.TryGetValues("Retry-After", out var values));
112+
Assert.Equal("7", Assert.Single(values));
113+
Assert.Equal(HttpStatusCode.TooManyRequests, retry.StatusCode);
114+
}
115+
69116
[Fact]
70117
public async Task Latency_AddsConfiguredDelayBeforeForwarding()
71118
{

‎DevProxy.Plugins/Behavior/GenericRandomErrorPlugin.cs‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -219,10 +219,11 @@ private void UpdateProxyResponse(ProxyRequestArgs e, GenericErrorResponseRespons
219219
headers.AddRange(error.Headers);
220220
}
221221

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

‎DevProxy.Plugins/packages.lock.json‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -437,10 +437,10 @@
437437
},
438438
"Newtonsoft.Json.Schema": {
439439
"type": "Transitive",
440-
"resolved": "4.0.1",
441-
"contentHash": "rbHUKp5WTIbqmLEeJ21nTTDGcfR0LA7bVMzm0bYc3yx6NFKiCIHzzvYbwA4Sqgs7+wNldc5nBlkbithWj8IZig==",
440+
"resolved": "4.0.2",
441+
"contentHash": "+Kyhf67t35xoUycpSkaaxGeBaTLz/sZOPEW94HCVM/WJFVJooIsIHGUV7hS8xSvT1Ci78XwN4Fr57927OGYE6w==",
442442
"dependencies": {
443-
"Newtonsoft.Json": "13.0.3"
443+
"Newtonsoft.Json": "13.0.4"
444444
}
445445
},
446446
"OpenTelemetry.Api": {
@@ -532,7 +532,7 @@
532532
"Microsoft.Extensions.Logging.Abstractions": "[10.0.12, )",
533533
"Microsoft.OpenApi": "[3.10.2, )",
534534
"Microsoft.OpenApi.YamlReader": "[3.10.2, )",
535-
"Newtonsoft.Json.Schema": "[4.0.1, )",
535+
"Newtonsoft.Json.Schema": "[4.0.2, )",
536536
"Scriban": "[7.5.0, )",
537537
"System.CommandLine": "[2.0.12, )",
538538
"YamlDotNet": "[18.1.0, )"

‎DevProxy.Proxy.Kestrel/packages.lock.json‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,15 +85,15 @@
8585
},
8686
"Newtonsoft.Json": {
8787
"type": "Transitive",
88-
"resolved": "13.0.3",
89-
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
88+
"resolved": "13.0.4",
89+
"contentHash": "pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A=="
9090
},
9191
"Newtonsoft.Json.Schema": {
9292
"type": "Transitive",
93-
"resolved": "4.0.1",
94-
"contentHash": "rbHUKp5WTIbqmLEeJ21nTTDGcfR0LA7bVMzm0bYc3yx6NFKiCIHzzvYbwA4Sqgs7+wNldc5nBlkbithWj8IZig==",
93+
"resolved": "4.0.2",
94+
"contentHash": "+Kyhf67t35xoUycpSkaaxGeBaTLz/sZOPEW94HCVM/WJFVJooIsIHGUV7hS8xSvT1Ci78XwN4Fr57927OGYE6w==",
9595
"dependencies": {
96-
"Newtonsoft.Json": "13.0.3"
96+
"Newtonsoft.Json": "13.0.4"
9797
}
9898
},
9999
"Scriban": {
@@ -150,7 +150,7 @@
150150
"Microsoft.EntityFrameworkCore.Sqlite": "[10.0.12, )",
151151
"Microsoft.OpenApi": "[3.10.2, )",
152152
"Microsoft.OpenApi.YamlReader": "[3.10.2, )",
153-
"Newtonsoft.Json.Schema": "[4.0.1, )",
153+
"Newtonsoft.Json.Schema": "[4.0.2, )",
154154
"Scriban": "[7.5.0, )",
155155
"System.CommandLine": "[2.0.12, )",
156156
"YamlDotNet": "[18.1.0, )"

‎DevProxy/Commands/ConfigCommand.cs‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ private enum ConfigFileFormat
4848
private readonly IProxyConfiguration _proxyConfiguration;
4949
private readonly HttpClient _httpClient;
5050
private const string ConfigTemplatesFolder = "config-templates";
51+
private static readonly string[] configFilePatterns = ["*.json", "*.yaml", "*.yml"];
5152

5253
public ConfigCommand(
5354
HttpClient httpClient,
@@ -309,6 +310,8 @@ private async Task DownloadConfigAsync(string configId, OutputFormat outputForma
309310
/// these file names. If there are no proxy configs, it'll return
310311
/// an array of all the mock files. If there are no mocks, it'll return
311312
/// an empty array indicating that there's no entry point.
313+
/// Files in the .devproxy subfolder are listed before files in the
314+
/// root folder.
312315
/// </remarks>
313316
/// <param name="configFolder">Full path to the folder with config files</param>
314317
/// <returns>Array of files that can be used to start proxy with</returns>
@@ -318,11 +321,14 @@ private ProxyConfigInfo GetConfigInfo(string configFolder)
318321

319322
_logger.LogDebug("Getting list of config files in {ConfigFolder}...", configFolder);
320323

321-
// Get both JSON and YAML files
322-
var jsonFiles = Directory.GetFiles(configFolder, "*.json");
323-
var yamlFiles = Directory.GetFiles(configFolder, "*.yaml");
324-
var ymlFiles = Directory.GetFiles(configFolder, "*.yml");
325-
var allConfigFiles = jsonFiles.Concat(yamlFiles).Concat(ymlFiles).ToArray();
324+
// Presets in the samples gallery keep their config in a .devproxy
325+
// subfolder, so scan it first, followed by the root folder
326+
var foldersToScan = new[] { Path.Combine(configFolder, ".devproxy"), configFolder }
327+
.Where(Directory.Exists);
328+
var allConfigFiles = foldersToScan
329+
.SelectMany(folder => configFilePatterns
330+
.SelectMany(pattern => Directory.GetFiles(folder, pattern)))
331+
.ToArray();
326332

327333
if (allConfigFiles.Length == 0)
328334
{

‎DevProxy/packages.lock.json‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -251,10 +251,10 @@
251251
},
252252
"Newtonsoft.Json.Schema": {
253253
"type": "Transitive",
254-
"resolved": "4.0.1",
255-
"contentHash": "rbHUKp5WTIbqmLEeJ21nTTDGcfR0LA7bVMzm0bYc3yx6NFKiCIHzzvYbwA4Sqgs7+wNldc5nBlkbithWj8IZig==",
254+
"resolved": "4.0.2",
255+
"contentHash": "+Kyhf67t35xoUycpSkaaxGeBaTLz/sZOPEW94HCVM/WJFVJooIsIHGUV7hS8xSvT1Ci78XwN4Fr57927OGYE6w==",
256256
"dependencies": {
257-
"Newtonsoft.Json": "13.0.3"
257+
"Newtonsoft.Json": "13.0.4"
258258
}
259259
},
260260
"OpenTelemetry.Api": {
@@ -337,7 +337,7 @@
337337
"Microsoft.EntityFrameworkCore.Sqlite": "[10.0.12, )",
338338
"Microsoft.OpenApi": "[3.10.2, )",
339339
"Microsoft.OpenApi.YamlReader": "[3.10.2, )",
340-
"Newtonsoft.Json.Schema": "[4.0.1, )",
340+
"Newtonsoft.Json.Schema": "[4.0.2, )",
341341
"Scriban": "[7.5.0, )",
342342
"System.CommandLine": "[2.0.12, )",
343343
"YamlDotNet": "[18.1.0, )"

0 commit comments

Comments
 (0)