Skip to content

Commit 7544807

Browse files
committed
Almost there
1 parent e4ad26f commit 7544807

File tree

9 files changed

+450
-408
lines changed

9 files changed

+450
-408
lines changed

DevProxy.Abstractions/Plugins/BaseReportingPlugin.cs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,19 @@ namespace DevProxy.Abstractions.Plugins;
1111

1212
public abstract class BaseReportingPlugin(
1313
ILogger logger,
14-
ISet<UrlToWatch> urlsToWatch) : BasePlugin(logger, urlsToWatch)
14+
ISet<UrlToWatch> urlsToWatch,
15+
IProxyStorage proxyStorage) : BasePlugin(logger, urlsToWatch)
1516
{
16-
protected virtual void StoreReport(object report, ProxyEventArgsBase e)
17+
protected IProxyStorage ProxyStorage => proxyStorage;
18+
protected virtual void StoreReport(object report)
1719
{
18-
ArgumentNullException.ThrowIfNull(e);
1920

2021
if (report is null)
2122
{
2223
return;
2324
}
2425

25-
((Dictionary<string, object>)e.GlobalData[ProxyUtils.ReportsKey])[Name] = report;
26+
((Dictionary<string, object>)ProxyStorage.GlobalData[ProxyUtils.ReportsKey])[Name] = report;
2627
}
2728
}
2829

@@ -31,23 +32,24 @@ public abstract class BaseReportingPlugin<TConfiguration>(
3132
ILogger logger,
3233
ISet<UrlToWatch> urlsToWatch,
3334
IProxyConfiguration proxyConfiguration,
34-
IConfigurationSection configurationSection) :
35+
IConfigurationSection configurationSection,
36+
IProxyStorage proxyStorage) :
3537
BasePlugin<TConfiguration>(
3638
httpClient,
3739
logger,
3840
urlsToWatch,
3941
proxyConfiguration,
4042
configurationSection) where TConfiguration : new()
4143
{
42-
protected virtual void StoreReport(object report, ProxyEventArgsBase e)
44+
protected IProxyStorage ProxyStorage => proxyStorage;
45+
protected virtual void StoreReport(object report)
4346
{
44-
ArgumentNullException.ThrowIfNull(e);
4547

4648
if (report is null)
4749
{
4850
return;
4951
}
5052

51-
((Dictionary<string, object>)e.GlobalData[ProxyUtils.ReportsKey])[Name] = report;
53+
((Dictionary<string, object>)ProxyStorage.GlobalData[ProxyUtils.ReportsKey])[Name] = report;
5254
}
5355
}

DevProxy.Abstractions/Proxy/ProxyEvents.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ public class RequestLog
5757
//public LoggingContext? Context { get; set; }
5858
[JsonIgnore]
5959
public HttpRequestMessage? Request { get; internal set; }
60+
61+
[JsonIgnore]
62+
public HttpResponseMessage? Response { get; internal set; }
63+
6064
public string Message { get; set; }
6165
public MessageType MessageType { get; set; }
6266
public string? Method { get; init; }
@@ -68,10 +72,11 @@ public RequestLog(string message, MessageType messageType, object? context)
6872
throw new NotImplementedException("This constructor is not implemented. Use the other constructors instead.");
6973
}
7074

71-
public RequestLog(string message, MessageType messageType, HttpRequestMessage requestMessage) :
75+
public RequestLog(string message, MessageType messageType, HttpRequestMessage requestMessage, HttpResponseMessage? responseMessage = null) :
7276
this(message, messageType, requestMessage?.Method.Method, requestMessage?.RequestUri!.AbsoluteUri, _: null)
7377
{
7478
Request = requestMessage;
79+
Response = responseMessage;
7580
}
7681

7782
public RequestLog(string message, MessageType messageType, string method, string url) :

DevProxy.Plugins/Behavior/RetryAfterPlugin.cs

Lines changed: 47 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -11,62 +11,60 @@
1111
using Microsoft.Extensions.Logging;
1212
using System.Globalization;
1313
using System.Net;
14+
using System.Text;
1415
using System.Text.Json;
1516
using System.Text.RegularExpressions;
16-
using Unobtanium.Web.Proxy.Http;
17-
using Unobtanium.Web.Proxy.Models;
1817

1918
namespace DevProxy.Plugins.Behavior;
2019

2120
public sealed class RetryAfterPlugin(
2221
ILogger<RetryAfterPlugin> logger,
23-
ISet<UrlToWatch> urlsToWatch) : BasePlugin(logger, urlsToWatch)
22+
ISet<UrlToWatch> urlsToWatch,
23+
IProxyStorage proxyStorage) : BasePlugin(logger, urlsToWatch)
2424
{
25+
private readonly IProxyStorage _proxyStorage = proxyStorage;
2526
public static readonly string ThrottledRequestsKey = "ThrottledRequests";
2627

2728
public override string Name => nameof(RetryAfterPlugin);
2829

29-
public override Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken cancellationToken)
30+
public override Func<RequestArguments, CancellationToken, Task<PluginResponse>>? OnRequestAsync => (args, cancellationToken) =>
3031
{
31-
Logger.LogTrace("{Method} called", nameof(BeforeRequestAsync));
32+
Logger.LogTrace("{Method} called", nameof(OnRequestAsync));
3233

33-
ArgumentNullException.ThrowIfNull(e);
34-
35-
if (!e.HasRequestUrlMatch(UrlsToWatch))
34+
if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, args.Request.RequestUri))
3635
{
37-
Logger.LogRequest("URL not matched", MessageType.Skipped, new(e.Session));
38-
return Task.CompletedTask;
36+
Logger.LogRequest("URL not matched", MessageType.Skipped, args.Request);
37+
return Task.FromResult(PluginResponse.Continue());
3938
}
40-
if (e.ResponseState.HasBeenSet)
39+
40+
if (args.Request.Method == HttpMethod.Options)
4141
{
42-
Logger.LogRequest("Response already set", MessageType.Skipped, new(e.Session));
43-
return Task.CompletedTask;
42+
Logger.LogRequest("Skipping OPTIONS request", MessageType.Skipped, args.Request);
43+
return Task.FromResult(PluginResponse.Continue());
4444
}
45-
if (string.Equals(e.Session.HttpClient.Request.Method, "OPTIONS", StringComparison.OrdinalIgnoreCase))
45+
46+
var throttleResponse = CheckIfThrottled(args.Request);
47+
if (throttleResponse != null)
4648
{
47-
Logger.LogRequest("Skipping OPTIONS request", MessageType.Skipped, new(e.Session));
48-
return Task.CompletedTask;
49+
return Task.FromResult(PluginResponse.Respond(throttleResponse));
4950
}
5051

51-
ThrottleIfNecessary(e);
52-
53-
Logger.LogTrace("Left {Name}", nameof(BeforeRequestAsync));
54-
return Task.CompletedTask;
55-
}
52+
Logger.LogTrace("Left {Name}", nameof(OnRequestAsync));
53+
return Task.FromResult(PluginResponse.Continue());
54+
};
5655

57-
private void ThrottleIfNecessary(ProxyRequestArgs e)
56+
private HttpResponseMessage? CheckIfThrottled(HttpRequestMessage request)
5857
{
59-
var request = e.Session.HttpClient.Request;
60-
if (!e.GlobalData.TryGetValue(ThrottledRequestsKey, out var value))
58+
if (!_proxyStorage.GlobalData.TryGetValue(ThrottledRequestsKey, out var value))
6159
{
62-
Logger.LogRequest("Request not throttled", MessageType.Skipped, new(e.Session));
63-
return;
60+
Logger.LogRequest("Request not throttled", MessageType.Skipped, request);
61+
return null;
6462
}
6563

6664
if (value is not List<ThrottlerInfo> throttledRequests)
6765
{
68-
Logger.LogRequest("Request not throttled", MessageType.Skipped, new(e.Session));
69-
return;
66+
Logger.LogRequest("Request not throttled", MessageType.Skipped, request);
67+
return null;
7068
}
7169

7270
var expiredThrottlers = throttledRequests.Where(t => t.ResetTime < DateTime.Now).ToArray();
@@ -77,32 +75,31 @@ private void ThrottleIfNecessary(ProxyRequestArgs e)
7775

7876
if (throttledRequests.Count == 0)
7977
{
80-
Logger.LogRequest("Request not throttled", MessageType.Skipped, new(e.Session));
81-
return;
78+
Logger.LogRequest("Request not throttled", MessageType.Skipped, request);
79+
return null;
8280
}
8381

8482
foreach (var throttler in throttledRequests)
8583
{
8684
var throttleInfo = throttler.ShouldThrottle(request, throttler.ThrottlingKey);
8785
if (throttleInfo.ThrottleForSeconds > 0)
8886
{
89-
var message = $"Calling {request.Url} before waiting for the Retry-After period. Request will be throttled. Throttling on {throttler.ThrottlingKey}.";
90-
Logger.LogRequest(message, MessageType.Failed, new(e.Session));
87+
var message = $"Calling {request.RequestUri} before waiting for the Retry-After period. Request will be throttled. Throttling on {throttler.ThrottlingKey}.";
88+
Logger.LogRequest(message, MessageType.Failed, request);
9189

9290
throttler.ResetTime = DateTime.Now.AddSeconds(throttleInfo.ThrottleForSeconds);
93-
UpdateProxyResponse(e, throttleInfo, string.Join(' ', message));
94-
return;
91+
return BuildThrottleResponse(request, throttleInfo, string.Join(' ', message));
9592
}
9693
}
9794

98-
Logger.LogRequest("Request not throttled", MessageType.Skipped, new(e.Session));
95+
Logger.LogRequest("Request not throttled", MessageType.Skipped, request);
96+
return null;
9997
}
10098

101-
private static void UpdateProxyResponse(ProxyRequestArgs e, ThrottlingInfo throttlingInfo, string message)
99+
private static HttpResponseMessage BuildThrottleResponse(HttpRequestMessage request, ThrottlingInfo throttlingInfo, string message)
102100
{
103101
var headers = new List<MockResponseHeader>();
104102
var body = string.Empty;
105-
var request = e.Session.HttpClient.Request;
106103

107104
// override the response body and headers for the error response
108105
if (ProxyUtils.IsGraphRequest(request))
@@ -128,7 +125,7 @@ private static void UpdateProxyResponse(ProxyRequestArgs e, ThrottlingInfo throt
128125
else
129126
{
130127
// ProxyUtils.BuildGraphResponseHeaders already includes CORS headers
131-
if (request.Headers.Any(h => h.Name.Equals("Origin", StringComparison.OrdinalIgnoreCase)))
128+
if (request.Headers.TryGetValues("Origin", out var _))
132129
{
133130
headers.Add(new("Access-Control-Allow-Origin", "*"));
134131
headers.Add(new("Access-Control-Expose-Headers", throttlingInfo.RetryAfterHeaderName));
@@ -137,9 +134,18 @@ private static void UpdateProxyResponse(ProxyRequestArgs e, ThrottlingInfo throt
137134

138135
headers.Add(new(throttlingInfo.RetryAfterHeaderName, throttlingInfo.ThrottleForSeconds.ToString(CultureInfo.InvariantCulture)));
139136

140-
e.Session.GenericResponse(body ?? string.Empty, HttpStatusCode.TooManyRequests, headers.Select(h => new HttpHeader(h.Name, h.Value)));
141-
e.ResponseState.HasBeenSet = true;
137+
var response = new HttpResponseMessage(HttpStatusCode.TooManyRequests)
138+
{
139+
Content = new StringContent(body ?? string.Empty, Encoding.UTF8, "application/json")
140+
};
141+
142+
foreach (var header in headers)
143+
{
144+
_ = response.Headers.TryAddWithoutValidation(header.Name, header.Value);
145+
}
146+
147+
return response;
142148
}
143149

144-
private static string BuildApiErrorMessage(Request r, string message) => $"{message} {(ProxyUtils.IsGraphRequest(r) ? ProxyUtils.IsSdkRequest(r) ? "" : string.Join(' ', MessageUtils.BuildUseSdkForErrorsMessage()) : "")}";
150+
private static string BuildApiErrorMessage(HttpRequestMessage r, string message) => $"{message} {(ProxyUtils.IsGraphRequest(r) ? ProxyUtils.IsSdkRequest(r) ? "" : string.Join(' ', MessageUtils.BuildUseSdkForErrorsMessage()) : "")}";
145151
}

DevProxy.Plugins/Generation/MockGeneratorPlugin.cs

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@
99
using DevProxy.Plugins.Mocking;
1010
using DevProxy.Plugins.Utils;
1111
using Microsoft.Extensions.Logging;
12+
using System.Net.Http.Json;
1213
using System.Text.Json;
13-
using Unobtanium.Web.Proxy.EventArguments;
1414

1515
namespace DevProxy.Plugins.Generation;
1616

1717
public sealed class MockGeneratorPlugin(
1818
ILogger<MockGeneratorPlugin> logger,
19-
ISet<UrlToWatch> urlsToWatch) : BaseReportingPlugin(logger, urlsToWatch)
19+
ISet<UrlToWatch> urlsToWatch,
20+
IProxyStorage proxyStorage) : BaseReportingPlugin(logger, urlsToWatch, proxyStorage)
2021
{
2122
public override string Name => nameof(MockGeneratorPlugin);
2223

@@ -42,9 +43,7 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation
4243
cancellationToken.ThrowIfCancellationRequested();
4344

4445
if (request.MessageType != MessageType.InterceptedResponse ||
45-
request.Context is null ||
46-
request.Context.Session is null ||
47-
!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, request.Context.Session.HttpClient.Request.RequestUri.AbsoluteUri))
46+
!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, request.Request!.RequestUri!.AbsoluteUri))
4847
{
4948
continue;
5049
}
@@ -53,10 +52,16 @@ request.Context.Session is null ||
5352
Logger.LogDebug("Processing request {MethodAndUrlString}...", methodAndUrlString);
5453

5554
var (method, url) = GetMethodAndUrl(methodAndUrlString);
56-
var response = request.Context.Session.HttpClient.Response;
55+
var response = request.Response;
56+
57+
if (response is null)
58+
{
59+
Logger.LogDebug("No response found for request {MethodAndUrlString}. Skipping", methodAndUrlString);
60+
continue;
61+
}
5762

5863
var newHeaders = new List<MockResponseHeader>();
59-
newHeaders.AddRange(response.Headers.Select(h => new MockResponseHeader(h.Name, h.Value)));
64+
newHeaders.AddRange(response.Headers.Select(h => new MockResponseHeader(h.Key, string.Join(';', h.Value))));
6065
var mock = new MockResponse
6166
{
6267
Request = new()
@@ -66,9 +71,9 @@ request.Context.Session is null ||
6671
},
6772
Response = new()
6873
{
69-
StatusCode = response.StatusCode,
74+
StatusCode = (int)response.StatusCode,
7075
Headers = newHeaders,
71-
Body = await GetResponseBodyAsync(request.Context.Session, cancellationToken)
76+
Body = await GetResponseBodyAsync(request.Response!, cancellationToken)
7277
}
7378
};
7479
// skip mock if it's 200 but has no body
@@ -97,7 +102,7 @@ request.Context.Session is null ||
97102

98103
Logger.LogInformation("Created mock file {FileName} with {MocksCount} mocks", fileName, mocks.Count);
99104

100-
StoreReport(fileName, e);
105+
StoreReport(fileName);
101106

102107
Logger.LogTrace("Left {Name}", nameof(AfterRecordingStopAsync));
103108
}
@@ -108,28 +113,24 @@ request.Context.Session is null ||
108113
/// </summary>
109114
/// <param name="session">Request session</param>
110115
/// <returns>Response body or @filename for binary responses</returns>
111-
private async Task<dynamic?> GetResponseBodyAsync(SessionEventArgs session, CancellationToken cancellationToken)
116+
private async Task<dynamic?> GetResponseBodyAsync(HttpResponseMessage response, CancellationToken cancellationToken)
112117
{
113118
Logger.LogDebug("Getting response body...");
114119

115-
var response = session.HttpClient.Response;
116-
if (response.ContentType is null || !response.HasBody)
120+
if (response.Content is null)
117121
{
118122
Logger.LogDebug("Response has no content-type set or has no body. Skipping");
119123
return null;
120124
}
121125

122-
if (response.ContentType.Contains("application/json", StringComparison.OrdinalIgnoreCase))
126+
if (response.Content.Headers.ContentType?.MediaType?.Contains("application/json", StringComparison.OrdinalIgnoreCase) ?? false)
123127
{
124128
Logger.LogDebug("Response is JSON");
125129

126130
try
127131
{
128132
Logger.LogDebug("Reading response body as string...");
129-
var body = response.IsBodyRead ? response.BodyString : await session.GetResponseBodyAsString(cancellationToken);
130-
Logger.LogDebug("Body: {Body}", body);
131-
Logger.LogDebug("Deserializing response body...");
132-
return JsonSerializer.Deserialize<dynamic>(body, ProxyUtils.JsonSerializerOptions);
133+
return await response.Content.ReadFromJsonAsync<dynamic>(ProxyUtils.JsonSerializerOptions, cancellationToken);
133134
}
134135
catch (Exception ex)
135136
{
@@ -144,7 +145,7 @@ request.Context.Session is null ||
144145
{
145146
var filename = $"response-{Guid.NewGuid()}.bin";
146147
Logger.LogDebug("Reading response body as bytes...");
147-
var body = await session.GetResponseBody(cancellationToken);
148+
var body = await response.Content.ReadAsByteArrayAsync(cancellationToken);
148149
Logger.LogDebug("Writing response body to {Filename}...", filename);
149150
await File.WriteAllBytesAsync(filename, body, cancellationToken);
150151
return $"@{filename}";

0 commit comments

Comments
 (0)