Skip to content

Commit 3fa3512

Browse files
authored
[http-fault-injector] Add support for interrupting request (#7698)
- Also add ASPNETCORE_URLS to Dockerfile to fix port bug
1 parent b7b097c commit 3fa3512

9 files changed

Lines changed: 642 additions & 21 deletions

File tree

tools/http-fault-injector/Azure.Sdk.Tools.HttpFaultInjector/FaultInjectingMiddleware.cs

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,10 @@ private async Task<UpstreamResponse> SendUpstreamRequest(HttpRequest request, st
6767

6868
using (var upstreamRequest = new HttpRequestMessage(new HttpMethod(request.Method), upstreamUri))
6969
{
70-
if (request.ContentLength > 0)
70+
upstreamRequest.Content = new StreamContent(request.Body);
71+
foreach (var header in request.Headers.Where(h => Utils.ContentRequestHeaders.Contains(h.Key)))
7172
{
72-
upstreamRequest.Content = new StreamContent(request.Body);
73-
foreach (var header in request.Headers.Where(h => Utils.ContentRequestHeaders.Contains(h.Key)))
74-
{
75-
upstreamRequest.Content.Headers.Add(header.Key, values: header.Value);
76-
}
73+
upstreamRequest.Content.Headers.Add(header.Key, values: header.Value);
7774
}
7875

7976
foreach (var header in request.Headers.Where(h => !Utils.ExcludedRequestHeaders.Contains(h.Key) && !Utils.ContentRequestHeaders.Contains(h.Key)))
@@ -125,7 +122,42 @@ private async Task<MemoryStream> BufferContentAsync(HttpContent content, Cancell
125122

126123
private async Task ProxyResponse(HttpContext context, string upstreamUri, string fault, CancellationToken cancellationToken)
127124
{
125+
switch (fault)
126+
{
127+
case "nq":
128+
// No request body, then wait indefinitely
129+
await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken);
130+
return;
131+
case "nqc":
132+
// No request body, then close (TCP FIN)
133+
Close(context);
134+
return;
135+
case "nqa":
136+
// No request body, then abort (TCP RST)
137+
Abort(context);
138+
return;
139+
case "pq":
140+
// Partial request (50% of body), then wait indefinitely
141+
await ReadPartialRequest(context.Request, cancellationToken);
142+
await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken);
143+
return;
144+
case "pqc":
145+
// Partial request (50% of body), then close (TCP FIN)
146+
await ReadPartialRequest(context.Request, cancellationToken);
147+
Close(context);
148+
return;
149+
case "pqa":
150+
// Partial request (50% of body), then abort (TCP RST)
151+
await ReadPartialRequest(context.Request, cancellationToken);
152+
Abort(context);
153+
return;
154+
default:
155+
// Fall through and read full request body
156+
break;
157+
}
158+
128159
UpstreamResponse upstreamResponse = await SendUpstreamRequest(context.Request, upstreamUri, cancellationToken);
160+
129161
switch (fault)
130162
{
131163
case "f":
@@ -139,12 +171,12 @@ private async Task ProxyResponse(HttpContext context, string upstreamUri, string
139171
return;
140172
case "pc":
141173
// Partial Response (full headers, 50% of body), then close (TCP FIN)
142-
await SendDownstreamResponse(context.Response,upstreamResponse, upstreamResponse.ContentLength / 2, cancellationToken);
174+
await SendDownstreamResponse(context.Response, upstreamResponse, upstreamResponse.ContentLength / 2, cancellationToken);
143175
Close(context);
144176
return;
145177
case "pa":
146178
// Partial Response (full headers, 50% of body), then abort (TCP RST)
147-
await SendDownstreamResponse(context.Response,upstreamResponse, upstreamResponse.ContentLength / 2, cancellationToken);
179+
await SendDownstreamResponse(context.Response, upstreamResponse, upstreamResponse.ContentLength / 2, cancellationToken);
148180
Abort(context);
149181
return;
150182
case "pn":
@@ -169,6 +201,36 @@ private async Task ProxyResponse(HttpContext context, string upstreamUri, string
169201
}
170202
}
171203

204+
private static async Task ReadPartialRequest(HttpRequest request, CancellationToken cancellationToken)
205+
{
206+
var contentLength = request.ContentLength
207+
?? throw new InvalidOperationException("Partial request options require content-length request headers");
208+
var bytesToRead = contentLength / 2;
209+
long totalBytesRead = 0;
210+
var buffer = ArrayPool<byte>.Shared.Rent(81920);
211+
try
212+
{
213+
while (true)
214+
{
215+
var bytesRead = await request.Body.ReadAsync(
216+
buffer,
217+
0,
218+
(int)Math.Min(buffer.Length, bytesToRead - totalBytesRead),
219+
cancellationToken
220+
);
221+
totalBytesRead += bytesRead;
222+
if (totalBytesRead >= bytesToRead || bytesRead == 0)
223+
{
224+
break;
225+
}
226+
}
227+
}
228+
finally
229+
{
230+
ArrayPool<byte>.Shared.Return(buffer);
231+
}
232+
}
233+
172234
private async Task SendDownstreamResponse(HttpResponse response, UpstreamResponse upstreamResponse, long contentBytes, CancellationToken cancellationToken)
173235
{
174236
response.StatusCode = upstreamResponse.StatusCode;

tools/http-fault-injector/Azure.Sdk.Tools.HttpFaultInjector/Utils.cs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,20 @@ public static class Utils
77
{
88
public static readonly IDictionary<string, string> FaultModes = new Dictionary<string, string>()
99
{
10-
{ "f", "Full response" },
11-
{ "p", "Partial Response (full headers, 50% of body), then wait indefinitely" },
12-
{"pc", "Partial Response (full headers, 50% of body), then close (TCP FIN)" },
13-
{"pa", "Partial Response (full headers, 50% of body), then abort (TCP RST)" },
14-
{"pn", "Partial Response (full headers, 50% of body), then finish normally" },
15-
{"n", "No response, then wait indefinitely"},
16-
{"nc", "No response, then close (TCP FIN)" },
17-
{"na", "No response, then abort (TCP RST)" }
10+
{ "f", "Full response" },
11+
{ "p", "Partial Response (full headers, 50% of body), then wait indefinitely" },
12+
{ "pc", "Partial Response (full headers, 50% of body), then close (TCP FIN)" },
13+
{ "pa", "Partial Response (full headers, 50% of body), then abort (TCP RST)" },
14+
{ "pn", "Partial Response (full headers, 50% of body), then finish normally" },
15+
{ "n", "No response, then wait indefinitely"},
16+
{ "nc", "No response, then close (TCP FIN)" },
17+
{ "na", "No response, then abort (TCP RST)" },
18+
{ "pq", "Partial request (50% of body), then wait indefinitely"},
19+
{"pqc", "Partial request (50% of body), then close (TCP FIN)"},
20+
{"pqa", "Partial request (50% of body), then abort (TCP RST)"},
21+
{ "nq", "No request body, then wait indefinitely"},
22+
{"nqc", "No request body, then close (TCP FIN)"},
23+
{"nqa", "No request body, then abort (TCP RST)"},
1824
};
1925

2026
public static readonly string[] ExcludedRequestHeaders = new string[] {

tools/http-fault-injector/Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ RUN dotnet dev-certs https
1313
EXPOSE 7777
1414
EXPOSE 7778
1515

16+
ENV ASPNETCORE_URLS=http://+:7777;https://+:7778
17+
1618
ENTRYPOINT [ "/root/.dotnet/tools/http-fault-injector" ]
1719

1820

tools/http-fault-injector/sample-clients/net/http-client/Program.cs

Lines changed: 83 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
using System;
1+
using System;
2+
using System.IO;
23
using System.Net.Http;
4+
using System.Text;
35
using System.Threading;
46
using System.Threading.Tasks;
57

@@ -9,11 +11,34 @@ class Program
911
{
1012
static async Task Main(string[] args)
1113
{
12-
var httpClient = new HttpClient(new FaultInjectionClientHandler(new Uri("http://localhost:7777")));
14+
var directClient = new HttpClient();
15+
var faultInjectionClient = new HttpClient(new FaultInjectionClientHandler(new Uri("http://localhost:7777")))
16+
{
17+
// Short timeout for testing no response
18+
Timeout = TimeSpan.FromSeconds(10)
19+
};
20+
21+
Console.WriteLine("Sending request directly...");
22+
await Test(directClient);
23+
24+
Console.WriteLine("Sending request through fault injector...");
25+
await Test(faultInjectionClient);
26+
}
27+
28+
private static async Task Test(HttpClient client)
29+
{
30+
var baseUrl = "http://localhost:5000";
31+
32+
var uploadStream = new LoggingStream(new MemoryStream(Encoding.UTF8.GetBytes(new string('a', 10 * 1024 * 1024))));
33+
34+
var response = await client.PutAsync(baseUrl + "/upload", new StreamContent(uploadStream));
1335

14-
Console.WriteLine("Sending request...");
15-
var response = await httpClient.GetAsync("https://www.example.org");
16-
Console.WriteLine(response.StatusCode);
36+
var content = await response.Content.ReadAsStringAsync();
37+
var shortContent = (content.Length <= 40 ? content : content.Substring(0, 40) + "...");
38+
39+
Console.WriteLine($"Status: {response.StatusCode}");
40+
Console.WriteLine($"Content: {shortContent}");
41+
Console.WriteLine($"Length: {content.Length}");
1742
}
1843

1944
class FaultInjectionClientHandler : HttpClientHandler
@@ -52,5 +77,58 @@ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage reques
5277
return base.SendAsync(request, cancellationToken);
5378
}
5479
}
80+
81+
class LoggingStream : Stream
82+
{
83+
private readonly Stream _stream;
84+
private long _totalBytesRead;
85+
86+
public LoggingStream(Stream stream)
87+
{
88+
_stream = stream;
89+
}
90+
91+
public override bool CanRead => _stream.CanRead;
92+
93+
public override bool CanSeek => _stream.CanSeek;
94+
95+
public override bool CanWrite => _stream.CanWrite;
96+
97+
public override long Length => _stream.Length;
98+
99+
public override long Position
100+
{
101+
get => _stream.Position;
102+
set => _stream.Position = value;
103+
}
104+
105+
public override void Flush()
106+
{
107+
_stream.Flush();
108+
}
109+
110+
public override int Read(byte[] buffer, int offset, int count)
111+
{
112+
var bytesRead = _stream.Read(buffer, offset, count);
113+
_totalBytesRead += bytesRead;
114+
Console.WriteLine($"Read(buffer: byte[{buffer.Length}], offset: {offset}, count: {count}) => {bytesRead} (total {_totalBytesRead})");
115+
return bytesRead;
116+
}
117+
118+
public override long Seek(long offset, SeekOrigin origin)
119+
{
120+
return _stream.Seek(offset, origin);
121+
}
122+
123+
public override void SetLength(long value)
124+
{
125+
_stream.SetLength(value);
126+
}
127+
128+
public override void Write(byte[] buffer, int offset, int count)
129+
{
130+
_stream.Write(buffer, offset, count);
131+
}
132+
}
55133
}
56134
}

0 commit comments

Comments
 (0)