Skip to content

Commit 94a4f2b

Browse files
Adds support for batching in GraphRandomErrorPlugin. Closes #8 (#299)
1 parent dae7ec0 commit 94a4f2b

File tree

4 files changed

+122
-15
lines changed

4 files changed

+122
-15
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Text.Json.Serialization;
5+
6+
namespace Microsoft365.DeveloperProxy.Abstractions;
7+
8+
public class GraphBatchResponsePayload {
9+
[JsonPropertyName("responses")]
10+
public GraphBatchResponsePayloadResponse[] Responses { get; set; } = Array.Empty<GraphBatchResponsePayloadResponse>();
11+
}
12+
13+
public class GraphBatchResponsePayloadResponse {
14+
[JsonPropertyName("id")]
15+
public string Id { get; set; } = string.Empty;
16+
[JsonPropertyName("status")]
17+
public int Status { get; set; } = 200;
18+
[JsonPropertyName("body")]
19+
public GraphBatchResponsePayloadResponseBody? Body { get; set; }
20+
[JsonPropertyName("headers")]
21+
public Dictionary<string, string>? Headers { get; set; }
22+
}
23+
24+
public class GraphBatchResponsePayloadResponseBody {
25+
[JsonPropertyName("error")]
26+
public GraphBatchResponsePayloadResponseBodyError? Error { get; set; }
27+
}
28+
29+
public class GraphBatchResponsePayloadResponseBodyError {
30+
[JsonPropertyName("code")]
31+
public string Code { get; set; } = string.Empty;
32+
[JsonPropertyName("message")]
33+
public string Message { get; set; } = string.Empty;
34+
}

m365-developer-proxy-abstractions/ProxyUtils.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ public static bool IsGraphUrl(Uri uri) =>
3131
public static bool IsGraphBatchUrl(Uri uri) =>
3232
uri.AbsoluteUri.EndsWith("/$batch", StringComparison.OrdinalIgnoreCase);
3333

34+
public static Uri GetAbsoluteRequestUrlFromBatch(Uri batchRequestUri, string relativeRequestUrl) {
35+
var hostName = batchRequestUri.Host;
36+
var graphVersion = batchRequestUri.Segments[1].TrimEnd('/');
37+
var absoluteRequestUrl = new Uri($"https://{hostName}/{graphVersion}{relativeRequestUrl}");
38+
return absoluteRequestUrl;
39+
}
40+
3441
public static bool IsSdkRequest(Request request) => request.Headers.HeaderExists("SdkVersion");
3542

3643
public static bool IsGraphBetaRequest(Request request) =>

m365-developer-proxy-plugins/GraphUtils.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,20 @@ namespace Microsoft365.DeveloperProxy.Plugins;
88
public class GraphUtils
99
{
1010
// throttle requests per workload
11-
public static string BuildThrottleKey(Request r)
11+
public static string BuildThrottleKey(Request r) => BuildThrottleKey(r.RequestUri);
12+
13+
public static string BuildThrottleKey(Uri uri)
1214
{
13-
if (r.RequestUri.Segments.Length < 3)
15+
if (uri.Segments.Length < 3)
1416
{
15-
return r.RequestUri.Host;
17+
return uri.Host;
1618
}
1719

1820
// first segment is /
1921
// second segment is Graph version (v1.0, beta)
2022
// third segment is the workload (users, groups, etc.)
2123
// segment can end with / if there are other segments following
22-
var workload = r.RequestUri.Segments[2].Trim('/');
24+
var workload = uri.Segments[2].Trim('/');
2325

2426
// TODO: handle 'me' which is a proxy to other resources
2527

m365-developer-proxy-plugins/RandomErrors/GraphRandomErrorPlugin.cs

Lines changed: 75 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@
77
using System.CommandLine.Invocation;
88
using System.Net;
99
using System.Text.Json;
10+
using System.Text.Json.Serialization;
1011
using System.Text.RegularExpressions;
1112
using Titanium.Web.Proxy.EventArguments;
1213
using Titanium.Web.Proxy.Http;
1314
using Titanium.Web.Proxy.Models;
1415

1516
namespace Microsoft365.DeveloperProxy.Plugins.RandomErrors;
1617
internal enum GraphRandomErrorFailMode {
17-
Throttled,
1818
Random,
1919
PassThru
2020
}
@@ -96,17 +96,57 @@ public GraphRandomErrorPlugin() {
9696
// uses config to determine if a request should be failed
9797
private GraphRandomErrorFailMode ShouldFail(ProxyRequestArgs e) => _random.Next(1, 100) <= _proxyConfiguration?.Rate ? GraphRandomErrorFailMode.Random : GraphRandomErrorFailMode.PassThru;
9898

99-
private void FailResponse(ProxyRequestArgs e, GraphRandomErrorFailMode failMode) {
100-
HttpStatusCode errorStatus;
101-
if (failMode == GraphRandomErrorFailMode.Throttled) {
102-
errorStatus = HttpStatusCode.TooManyRequests;
99+
private void FailResponse(ProxyRequestArgs e) {
100+
// pick a random error response for the current request method
101+
var methodStatusCodes = _methodStatusCode[e.Session.HttpClient.Request.Method];
102+
var errorStatus = methodStatusCodes[_random.Next(0, methodStatusCodes.Length)];
103+
UpdateProxyResponse(e, errorStatus);
104+
}
105+
106+
private void FailBatch(ProxyRequestArgs e) {
107+
var batchResponse = new GraphBatchResponsePayload();
108+
109+
var batch = JsonSerializer.Deserialize<GraphBatchRequestPayload>(e.Session.HttpClient.Request.BodyString);
110+
if (batch == null) {
111+
UpdateProxyBatchResponse(e, batchResponse);
112+
return;
103113
}
104-
else {
105-
// pick a random error response for the current request method
106-
var methodStatusCodes = _methodStatusCode[e.Session.HttpClient.Request.Method];
107-
errorStatus = methodStatusCodes[_random.Next(0, methodStatusCodes.Length)];
114+
115+
var responses = new List<GraphBatchResponsePayloadResponse>();
116+
foreach (var request in batch.Requests)
117+
{
118+
try {
119+
// pick a random error response for the current request method
120+
var methodStatusCodes = _methodStatusCode[request.Method];
121+
var errorStatus = methodStatusCodes[_random.Next(0, methodStatusCodes.Length)];
122+
123+
var response = new GraphBatchResponsePayloadResponse {
124+
Id = request.Id,
125+
Status = (int)errorStatus,
126+
Body = new GraphBatchResponsePayloadResponseBody {
127+
Error = new GraphBatchResponsePayloadResponseBodyError {
128+
Code = new Regex("([A-Z])").Replace(errorStatus.ToString(), m => { return $" {m.Groups[1]}"; }).Trim(),
129+
Message = "Some error was generated by the proxy.",
130+
}
131+
}
132+
};
133+
134+
if (errorStatus == HttpStatusCode.TooManyRequests) {
135+
var retryAfterDate = DateTime.Now.AddSeconds(retryAfterInSeconds);
136+
var requestUrl = ProxyUtils.GetAbsoluteRequestUrlFromBatch(e.Session.HttpClient.Request.RequestUri, request.Url);
137+
e.ThrottledRequests.Add(new ThrottlerInfo(GraphUtils.BuildThrottleKey(requestUrl), ShouldThrottle, retryAfterDate));
138+
response.Headers = new Dictionary<string, string>{
139+
{ "Retry-After", retryAfterInSeconds.ToString() }
140+
};
141+
}
142+
143+
responses.Add(response);
144+
}
145+
catch {}
108146
}
109-
UpdateProxyResponse(e, errorStatus);
147+
batchResponse.Responses = responses.ToArray();
148+
149+
UpdateProxyBatchResponse(e, batchResponse);
110150
}
111151

112152
private ThrottlingInfo ShouldThrottle(Request request, string throttlingKey) {
@@ -139,6 +179,25 @@ private void UpdateProxyResponse(ProxyRequestArgs ev, HttpStatusCode errorStatus
139179
_logger?.LogRequest(new[] { $"{(int)errorStatus} {errorStatus.ToString()}" }, MessageType.Chaos, new LoggingContext(ev.Session));
140180
session.GenericResponse(body ?? string.Empty, errorStatus, headers);
141181
}
182+
183+
private void UpdateProxyBatchResponse(ProxyRequestArgs ev, GraphBatchResponsePayload response) {
184+
// failed batch uses a fixed 424 error status code
185+
var errorStatus = HttpStatusCode.FailedDependency;
186+
187+
SessionEventArgs session = ev.Session;
188+
string requestId = Guid.NewGuid().ToString();
189+
string requestDate = DateTime.Now.ToString();
190+
Request request = session.HttpClient.Request;
191+
var headers = ProxyUtils.BuildGraphResponseHeaders(request, requestId, requestDate);
192+
193+
var options = new JsonSerializerOptions {
194+
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
195+
};
196+
string body = JsonSerializer.Serialize(response, options);
197+
_logger?.LogRequest(new[] { $"{(int)errorStatus} {errorStatus.ToString()}" }, MessageType.Chaos, new LoggingContext(ev.Session));
198+
session.GenericResponse(body, errorStatus, headers);
199+
}
200+
142201
private static string BuildApiErrorMessage(Request r) => $"Some error was generated by the proxy. {(ProxyUtils.IsGraphRequest(r) ? ProxyUtils.IsSdkRequest(r) ? "" : String.Join(' ', MessageUtils.BuildUseSdkForErrorsMessage(r)) : "")}";
143202

144203
public override void Register(IPluginEvents pluginEvents,
@@ -189,7 +248,12 @@ private async Task OnRequest(object? sender, ProxyRequestArgs e) {
189248
if (failMode == GraphRandomErrorFailMode.PassThru && _proxyConfiguration?.Rate != 100) {
190249
return;
191250
}
192-
FailResponse(e, failMode);
251+
if (ProxyUtils.IsGraphBatchUrl(e.Session.HttpClient.Request.RequestUri)) {
252+
FailBatch(e);
253+
}
254+
else {
255+
FailResponse(e);
256+
}
193257
state.HasBeenSet = true;
194258
}
195259
}

0 commit comments

Comments
 (0)