From 0788a7b26e8472afc034a4b1e8177ce99ed7aeca Mon Sep 17 00:00:00 2001 From: Waldek Mastykarz Date: Wed, 21 Oct 2026 15:56:28 +0200 Subject: [PATCH] Pass through GraphRandomErrorPlugin requests without allowed errors. Closes #1956 When allowedErrors leaves no status codes for a request method, the plugin indexed an empty array and threw. Requests whose method has no allowed error codes now pass through. Graph batches pass through when any of their requests can't be failed, since a batch response must cover every request. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5c346308-7164-42a0-8d8e-91f93ffd42ef --- .../ManipulationAndMockingIntegrationTests.cs | 55 +++++++++++++++++++ .../Behavior/GraphRandomErrorPlugin.cs | 34 ++++++++++-- 2 files changed, 83 insertions(+), 6 deletions(-) diff --git a/DevProxy.Integration.Tests/ManipulationAndMockingIntegrationTests.cs b/DevProxy.Integration.Tests/ManipulationAndMockingIntegrationTests.cs index 9abdb867..66d6df47 100644 --- a/DevProxy.Integration.Tests/ManipulationAndMockingIntegrationTests.cs +++ b/DevProxy.Integration.Tests/ManipulationAndMockingIntegrationTests.cs @@ -2,9 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.CommandLine; using System.Net; using System.Net.Sockets; using System.Text; +using DevProxy.Abstractions.Proxy; using DevProxy.Plugins.Behavior; using DevProxy.Plugins.Manipulation; using DevProxy.Plugins.Mocking; @@ -450,4 +452,57 @@ public async Task GraphRandomError_Rate100_FailsEveryMatchedRequest() (int)response.StatusCode >= 400, $"Expected an injected 4xx/5xx error, saw {(int)response.StatusCode}."); } + + private static GraphRandomErrorPlugin CreateGraphRandomErrorPlugin(string configJson) + { + var plugin = new GraphRandomErrorPlugin( + SharedHttpClient, + NullLogger.Instance, + KestrelProxyHarness.BuildUrlsToWatch("graph.microsoft.com"), + ProxyConfig, + PluginConfig.FromJson(configJson)); + var command = new RootCommand(); + foreach (var option in plugin.GetOptions()) + { + command.Options.Add(option); + } + plugin.OptionsLoaded(new OptionsLoadedArgs(command.Parse([]))); + return plugin; + } + + [Fact] + public async Task GraphRandomError_NoAllowedErrorsForMethod_PassesThrough() + { + // 507 isn't a built-in error for GET, so GET has no errors left to + // throw while POST does. + var plugin = CreateGraphRandomErrorPlugin("""{ "rate": 100, "allowedErrors": [507] }"""); + + var get = TestExchange.Request("GET", "https://graph.microsoft.com/v1.0/me"); + await plugin.BeforeRequestAsync(get.RequestArgs, CancellationToken.None); + Assert.False(get.State.HasBeenSet); + + var post = TestExchange.Request("POST", "https://graph.microsoft.com/v1.0/me/messages", body: "{}"); + await plugin.BeforeRequestAsync(post.RequestArgs, CancellationToken.None); + Assert.True(post.State.HasBeenSet); + } + + [Fact] + public async Task GraphRandomError_BatchWithMethodWithoutAllowedErrors_PassesThrough() + { + var plugin = CreateGraphRandomErrorPlugin("""{ "rate": 100, "allowedErrors": [507] }"""); + + var batch = TestExchange.Request( + "POST", + "https://graph.microsoft.com/v1.0/$batch", + body: """ + { + "requests": [ + { "id": "1", "method": "GET", "url": "/me" }, + { "id": "2", "method": "POST", "url": "/me/messages", "body": {}, "headers": { "content-type": "application/json" } } + ] + } + """); + await plugin.BeforeRequestAsync(batch.RequestArgs, CancellationToken.None); + Assert.False(batch.State.HasBeenSet); + } } diff --git a/DevProxy.Plugins/Behavior/GraphRandomErrorPlugin.cs b/DevProxy.Plugins/Behavior/GraphRandomErrorPlugin.cs index 1af35fa0..11372aee 100644 --- a/DevProxy.Plugins/Behavior/GraphRandomErrorPlugin.cs +++ b/DevProxy.Plugins/Behavior/GraphRandomErrorPlugin.cs @@ -193,11 +193,16 @@ public override Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken ca } if (ProxyUtils.IsGraphBatchUrl(e.ProxySession.Request.RequestUri)) { - FailBatch(e); + if (!FailBatch(e)) + { + Logger.LogRequest("No allowed errors for one or more batch request methods", MessageType.Skipped, new LoggingContext(e.ProxySession)); + return Task.CompletedTask; + } } - else + else if (!FailResponse(e)) { - FailResponse(e); + Logger.LogRequest($"No allowed errors for {e.ProxySession.Request.Method} requests", MessageType.Skipped, new LoggingContext(e.ProxySession)); + return Task.CompletedTask; } state.HasBeenSet = true; @@ -208,15 +213,24 @@ public override Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken ca // uses config to determine if a request should be failed private GraphRandomErrorFailMode ShouldFail() => _random.Next(0, 100) < Configuration.Rate ? GraphRandomErrorFailMode.Random : GraphRandomErrorFailMode.PassThru; - private void FailResponse(ProxyRequestArgs e) + private bool HasAllowedErrors(string method) => + _methodStatusCode.TryGetValue(method, out var statusCodes) && statusCodes.Length > 0; + + private bool FailResponse(ProxyRequestArgs e) { + if (!HasAllowedErrors(e.ProxySession.Request.Method)) + { + return false; + } + // pick a random error response for the current request method var methodStatusCodes = _methodStatusCode[e.ProxySession.Request.Method]; var errorStatus = methodStatusCodes[_random.Next(0, methodStatusCodes.Length)]; UpdateProxyResponse(e, errorStatus); + return true; } - private void FailBatch(ProxyRequestArgs e) + private bool FailBatch(ProxyRequestArgs e) { var batchResponse = new GraphBatchResponsePayload(); @@ -224,7 +238,14 @@ private void FailBatch(ProxyRequestArgs e) if (batch == null) { UpdateProxyBatchResponse(e, batchResponse); - return; + return true; + } + + // a batch response must contain a response for every request, so if + // any request can't be failed, pass the whole batch through + if (batch.Requests.Any(r => !HasAllowedErrors(r.Method))) + { + return false; } var responses = new List(); @@ -276,6 +297,7 @@ private void FailBatch(ProxyRequestArgs e) batchResponse.Responses = [.. responses]; UpdateProxyBatchResponse(e, batchResponse); + return true; } private ThrottlingInfo ShouldThrottle(IHttpRequest request, string throttlingKey)