Skip to content

Commit fc6c0a1

Browse files
Extends GenericRandomErrorPlugin with request matching. Closes #818 (#819)
1 parent 7bbfb4a commit fc6c0a1

File tree

5 files changed

+239
-102
lines changed

5 files changed

+239
-102
lines changed
Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,40 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
using Microsoft.DevProxy.Abstractions;
5-
64
namespace Microsoft.DevProxy.Plugins;
75

86
public class GenericErrorResponse
97
{
10-
public int StatusCode { get; set; }
11-
public List<MockResponseHeader>? Headers { get; set; }
8+
public GenericErrorResponseRequest? Request { get; set; }
9+
public GenericErrorResponseResponse[]? Responses { get; set; }
10+
}
11+
12+
public class GenericErrorResponseRequest
13+
{
14+
public string Url { get; set; } = string.Empty;
15+
public string Method { get; set; } = "GET";
16+
public string? BodyFragment { get; set; }
17+
}
18+
19+
public class GenericErrorResponseResponse
20+
{
21+
public int? StatusCode { get; set; } = 400;
1222
public dynamic? Body { get; set; }
23+
public List<GenericErrorResponseHeader>? Headers { get; set; }
24+
}
25+
26+
public class GenericErrorResponseHeader
27+
{
28+
public string Name { get; set; } = string.Empty;
29+
public string Value { get; set; } = string.Empty;
30+
31+
public GenericErrorResponseHeader()
32+
{
33+
}
34+
35+
public GenericErrorResponseHeader(string name, string value)
36+
{
37+
Name = name;
38+
Value = value;
39+
}
1340
}

dev-proxy-plugins/RandomErrors/GenericErrorResponsesLoader.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public void LoadResponses()
2626
if (!File.Exists(_errorsFile))
2727
{
2828
_logger.LogWarning("File {configurationFile} not found in the current directory. No error responses will be loaded", _configuration.ErrorsFile);
29-
_configuration.Responses = Array.Empty<GenericErrorResponse>();
29+
_configuration.Errors = Array.Empty<GenericErrorResponse>();
3030
return;
3131
}
3232

@@ -36,10 +36,10 @@ public void LoadResponses()
3636
using var reader = new StreamReader(stream);
3737
var responsesString = reader.ReadToEnd();
3838
var responsesConfig = JsonSerializer.Deserialize<GenericRandomErrorConfiguration>(responsesString, ProxyUtils.JsonSerializerOptions);
39-
IEnumerable<GenericErrorResponse>? configResponses = responsesConfig?.Responses;
39+
IEnumerable<GenericErrorResponse>? configResponses = responsesConfig?.Errors;
4040
if (configResponses is not null)
4141
{
42-
_configuration.Responses = configResponses;
42+
_configuration.Errors = configResponses;
4343
_logger.LogInformation("{configResponseCount} error responses loaded from {errorFile}", configResponses.Count(), _configuration.ErrorsFile);
4444
}
4545
}

dev-proxy-plugins/RandomErrors/GenericRandomErrorPlugin.cs

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using Titanium.Web.Proxy.Models;
1111
using Microsoft.DevProxy.Plugins.Behavior;
1212
using Microsoft.Extensions.Logging;
13+
using System.Text.RegularExpressions;
1314

1415
namespace Microsoft.DevProxy.Plugins.RandomErrors;
1516
internal enum GenericRandomErrorFailMode
@@ -23,7 +24,7 @@ public class GenericRandomErrorConfiguration
2324
{
2425
public string? ErrorsFile { get; set; }
2526
public int RetryAfterInSeconds { get; set; } = 5;
26-
public IEnumerable<GenericErrorResponse> Responses { get; set; } = Array.Empty<GenericErrorResponse>();
27+
public IEnumerable<GenericErrorResponse> Errors { get; set; } = Array.Empty<GenericErrorResponse>();
2728
}
2829

2930
public class GenericRandomErrorPlugin : BaseProxyPlugin
@@ -43,11 +44,16 @@ public GenericRandomErrorPlugin(IPluginEvents pluginEvents, IProxyContext contex
4344
// uses config to determine if a request should be failed
4445
private GenericRandomErrorFailMode ShouldFail(ProxyRequestArgs e) => _random.Next(1, 100) <= Context.Configuration.Rate ? GenericRandomErrorFailMode.Random : GenericRandomErrorFailMode.PassThru;
4546

46-
private void FailResponse(ProxyRequestArgs e, GenericRandomErrorFailMode failMode)
47+
private void FailResponse(ProxyRequestArgs e)
4748
{
48-
// pick a random error response for the current request
49-
var error = _configuration.Responses.ElementAt(_random.Next(0, _configuration.Responses.Count()));
50-
UpdateProxyResponse(e, error);
49+
var matchingResponse = GetMatchingErrorResponse(e.Session.HttpClient.Request);
50+
if (matchingResponse is not null &&
51+
matchingResponse.Responses is not null)
52+
{
53+
// pick a random error response for the current request
54+
var error = matchingResponse.Responses.ElementAt(_random.Next(0, matchingResponse.Responses.Length));
55+
UpdateProxyResponse(e, error);
56+
}
5157
}
5258

5359
private ThrottlingInfo ShouldThrottle(Request request, string throttlingKey)
@@ -56,16 +62,76 @@ private ThrottlingInfo ShouldThrottle(Request request, string throttlingKey)
5662
return new ThrottlingInfo(throttleKeyForRequest == throttlingKey ? _configuration.RetryAfterInSeconds : 0, "Retry-After");
5763
}
5864

59-
private void UpdateProxyResponse(ProxyRequestArgs e, GenericErrorResponse error)
65+
private GenericErrorResponse? GetMatchingErrorResponse(Request request)
66+
{
67+
if (_configuration.Errors is null ||
68+
!_configuration.Errors.Any())
69+
{
70+
return null;
71+
}
72+
73+
var errorResponse = _configuration.Errors.FirstOrDefault(errorResponse =>
74+
{
75+
if (errorResponse.Request is null) return false;
76+
if (errorResponse.Responses is null) return false;
77+
78+
if (errorResponse.Request.Method != request.Method) return false;
79+
if (errorResponse.Request.Url == request.Url &&
80+
HasMatchingBody(errorResponse, request))
81+
{
82+
return true;
83+
}
84+
85+
// check if the URL contains a wildcard
86+
// if it doesn't, it's not a match for the current request for sure
87+
if (!errorResponse.Request.Url.Contains('*'))
88+
{
89+
return false;
90+
}
91+
92+
// turn mock URL with wildcard into a regex and match against the request URL
93+
var errorResponseUrlRegex = Regex.Escape(errorResponse.Request.Url).Replace("\\*", ".*");
94+
return Regex.IsMatch(request.Url, $"^{errorResponseUrlRegex}$") &&
95+
HasMatchingBody(errorResponse, request);
96+
});
97+
98+
return errorResponse;
99+
}
100+
101+
private bool HasMatchingBody(GenericErrorResponse errorResponse, Request request)
102+
{
103+
if (request.Method == "GET")
104+
{
105+
// GET requests don't have a body so we can't match on it
106+
return true;
107+
}
108+
109+
if (errorResponse.Request?.BodyFragment is null)
110+
{
111+
// no body fragment to match on
112+
return true;
113+
}
114+
115+
if (!request.HasBody || string.IsNullOrEmpty(request.BodyString))
116+
{
117+
// error response defines a body fragment but the request has no body
118+
// so it can't match
119+
return false;
120+
}
121+
122+
return request.BodyString.Contains(errorResponse.Request.BodyFragment, StringComparison.OrdinalIgnoreCase);
123+
}
124+
125+
private void UpdateProxyResponse(ProxyRequestArgs e, GenericErrorResponseResponse error)
60126
{
61127
SessionEventArgs session = e.Session;
62128
Request request = session.HttpClient.Request;
63-
var headers = new List<MockResponseHeader>();
129+
var headers = new List<GenericErrorResponseHeader>();
64130
if (error.Headers is not null)
65131
{
66132
headers.AddRange(error.Headers);
67133
}
68-
134+
69135
if (error.StatusCode == (int)HttpStatusCode.TooManyRequests &&
70136
error.Headers is not null &&
71137
error.Headers.FirstOrDefault(h => h.Name == "Retry-After" || h.Name == "retry-after")?.Value == "@dynamic")
@@ -83,7 +149,7 @@ error.Headers is not null &&
83149
headers.Add(new("Retry-After", _configuration.RetryAfterInSeconds.ToString()));
84150
}
85151

86-
var statusCode = (HttpStatusCode)error.StatusCode;
152+
var statusCode = (HttpStatusCode)(error.StatusCode ?? 400);
87153
var body = error.Body is null ? string.Empty : JsonSerializer.Serialize(error.Body, ProxyUtils.JsonSerializerOptions);
88154
// we get a JSON string so need to start with the opening quote
89155
if (body.StartsWith("\"@"))
@@ -146,7 +212,7 @@ private Task OnRequest(object? sender, ProxyRequestArgs e)
146212
{
147213
return Task.CompletedTask;
148214
}
149-
FailResponse(e, failMode);
215+
FailResponse(e);
150216
state.HasBeenSet = true;
151217
}
152218

dev-proxy/devproxy-errors.json

Lines changed: 64 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,74 @@
11
{
22
"$schema": "https://raw.githubusercontent.com/microsoft/dev-proxy/main/schemas/v0.20.0/genericrandomerrorplugin.schema.json",
3-
"responses": [
3+
"errors": [
44
{
5-
"statusCode": 400,
6-
"body": {
7-
"message": "Bad Request",
8-
"details": "The server cannot process the request due to invalid syntax."
9-
}
10-
},
11-
{
12-
"statusCode": 401,
13-
"body": {
14-
"message": "Unauthorized",
15-
"details": "The request requires user authentication."
16-
}
17-
},
18-
{
19-
"statusCode": 403,
20-
"body": {
21-
"message": "Forbidden",
22-
"details": "The server understood the request, but refuses to authorize it."
23-
}
24-
},
25-
{
26-
"statusCode": 404,
27-
"body": {
28-
"message": "Not Found",
29-
"details": "The requested resource could not be found."
30-
}
31-
},
32-
{
33-
"statusCode": 418,
34-
"body": {
35-
"message": "I'm a teapot",
36-
"details": "The server refuses the attempt to brew coffee with a teapot."
37-
}
38-
},
39-
{
40-
"statusCode": 429,
41-
"body": {
42-
"message": "Too Many Requests",
43-
"details": "The user has sent too many requests in a given amount of time (\"rate limiting\")."
5+
"request": {
6+
"url": "https://jsonplaceholder.typicode.com/*"
447
},
45-
"headers": [
8+
"responses": [
9+
{
10+
"statusCode": 400,
11+
"body": {
12+
"message": "Bad Request",
13+
"details": "The server cannot process the request due to invalid syntax."
14+
}
15+
},
16+
{
17+
"statusCode": 401,
18+
"body": {
19+
"message": "Unauthorized",
20+
"details": "The request requires user authentication."
21+
}
22+
},
4623
{
47-
"name": "Retry-After",
48-
"value": "@dynamic"
24+
"statusCode": 403,
25+
"body": {
26+
"message": "Forbidden",
27+
"details": "The server understood the request, but refuses to authorize it."
28+
}
29+
},
30+
{
31+
"statusCode": 404,
32+
"body": {
33+
"message": "Not Found",
34+
"details": "The requested resource could not be found."
35+
}
36+
},
37+
{
38+
"statusCode": 418,
39+
"body": {
40+
"message": "I'm a teapot",
41+
"details": "The server refuses the attempt to brew coffee with a teapot."
42+
}
43+
},
44+
{
45+
"statusCode": 429,
46+
"body": {
47+
"message": "Too Many Requests",
48+
"details": "The user has sent too many requests in a given amount of time (\"rate limiting\")."
49+
},
50+
"headers": [
51+
{
52+
"name": "Retry-After",
53+
"value": "@dynamic"
54+
}
55+
]
56+
},
57+
{
58+
"statusCode": 500,
59+
"body": {
60+
"message": "Internal Server Error",
61+
"details": "The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application."
62+
}
63+
},
64+
{
65+
"statusCode": 503,
66+
"body": {
67+
"message": "Service Unavailable",
68+
"details": "The server is currently unable to handle the request due to a temporary overload or maintenance. Please try again later."
69+
}
4970
}
5071
]
51-
},
52-
{
53-
"statusCode": 500,
54-
"body": {
55-
"message": "Internal Server Error",
56-
"details": "The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application."
57-
}
58-
},
59-
{
60-
"statusCode": 503,
61-
"body": {
62-
"message": "Service Unavailable",
63-
"details": "The server is currently unable to handle the request due to a temporary overload or maintenance. Please try again later."
64-
}
6572
}
6673
]
6774
}

0 commit comments

Comments
 (0)