Skip to content

Commit 4f5a9f2

Browse files
Fix GZip handling for requests (#4165)
* Fix GZip handling for requests * Flush GZipStream when compressing. * Removing flush call - not needed for Test Proxy target platforms * repair record mode operation. we were exhausting the request body stream. * Update mock handler Co-authored-by: scbedd <[email protected]>
1 parent 7411d39 commit 4f5a9f2

File tree

6 files changed

+239
-50
lines changed

6 files changed

+239
-50
lines changed

tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/PlaybackTests.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.IO;
77
using System.Linq;
88
using System.Threading.Tasks;
9+
using Azure.Sdk.Tools.TestProxy.Common;
910
using Xunit;
1011

1112
namespace Azure.Sdk.Tools.TestProxy.Tests
@@ -217,5 +218,35 @@ public async Task TestPlaybackSetsRetryAfterToZero()
217218
await testRecordingHandler.HandlePlaybackRequest(recordingId, request, response);
218219
Assert.False(response.Headers.ContainsKey("Retry-After"));
219220
}
221+
222+
[Fact]
223+
public async Task TestPlaybackWithGZippedContentPlayback()
224+
{
225+
RecordingHandler testRecordingHandler = new RecordingHandler(Directory.GetCurrentDirectory());
226+
var httpContext = new DefaultHttpContext();
227+
var body = "{\"x-recording-file\":\"Test.RecordEntries/request_response_with_gzipped_content.json\"}";
228+
httpContext.Request.Body = TestHelpers.GenerateStreamRequestBody(body);
229+
httpContext.Request.ContentLength = body.Length;
230+
231+
var controller = new Playback(testRecordingHandler, new NullLoggerFactory())
232+
{
233+
ControllerContext = new ControllerContext()
234+
{
235+
HttpContext = httpContext
236+
}
237+
};
238+
await controller.Start();
239+
240+
var recordingId = httpContext.Response.Headers["x-recording-id"].ToString();
241+
Assert.NotNull(recordingId);
242+
Assert.True(testRecordingHandler.PlaybackSessions.ContainsKey(recordingId));
243+
var entry = testRecordingHandler.PlaybackSessions[recordingId].Session.Entries[0];
244+
HttpRequest request = TestHelpers.CreateRequestFromEntry(entry);
245+
246+
// compress the body to simulate what the request coming from the library will look like
247+
request.Body = new MemoryStream(GZipUtilities.CompressBody(BinaryData.FromStream(request.Body).ToArray(), request.Headers));
248+
HttpResponse response = new DefaultHttpContext().Response;
249+
await testRecordingHandler.HandlePlaybackRequest(recordingId, request, response);
250+
}
220251
}
221252
}

tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/RecordingHandlerTests.cs

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using System.Linq;
1313
using System.Net;
1414
using System.Net.Http;
15+
using System.Net.Http.Headers;
1516
using System.Text;
1617
using System.Text.Json;
1718
using System.Threading;
@@ -696,6 +697,52 @@ public async Task TestRecordMaintainsUpstreamOverrideHostHeader(string upstreamH
696697
}
697698
}
698699

700+
[Fact]
701+
public async Task TestRecordWithGZippedContent()
702+
{
703+
var httpContext = new DefaultHttpContext();
704+
var bodyBytes = Encoding.UTF8.GetBytes("{\"hello\":\"world\"}");
705+
var mockClient = new HttpClient(new MockHttpHandler(bodyBytes, "application/json", "gzip"));
706+
var path = Directory.GetCurrentDirectory();
707+
var recordingHandler = new RecordingHandler(path)
708+
{
709+
RedirectableClient = mockClient,
710+
RedirectlessClient = mockClient
711+
};
712+
713+
var relativePath = "recordings/gzip";
714+
var fullPathToRecording = Path.Combine(path, relativePath) + ".json";
715+
716+
await recordingHandler.StartRecordingAsync(relativePath, httpContext.Response);
717+
718+
var recordingId = httpContext.Response.Headers["x-recording-id"].ToString();
719+
720+
httpContext.Request.ContentType = "application/json";
721+
httpContext.Request.Headers["Content-Encoding"] = "gzip";
722+
httpContext.Request.ContentLength = 0;
723+
httpContext.Request.Headers["x-recording-id"] = recordingId;
724+
httpContext.Request.Headers["x-recording-upstream-base-uri"] = "http://example.org";
725+
httpContext.Request.Method = "GET";
726+
httpContext.Request.Body = new MemoryStream(GZipUtilities.CompressBody(bodyBytes, httpContext.Request.Headers));
727+
728+
await recordingHandler.HandleRecordRequestAsync(recordingId, httpContext.Request, httpContext.Response);
729+
recordingHandler.StopRecording(recordingId);
730+
731+
try
732+
{
733+
using var fileStream = File.Open(fullPathToRecording, FileMode.Open);
734+
using var doc = JsonDocument.Parse(fileStream);
735+
var record = RecordSession.Deserialize(doc.RootElement);
736+
var entry = record.Entries.First();
737+
Assert.Equal("{\"hello\":\"world\"}", Encoding.UTF8.GetString(entry.Request.Body));
738+
Assert.Equal("{\"hello\":\"world\"}", Encoding.UTF8.GetString(entry.Response.Body));
739+
}
740+
finally
741+
{
742+
File.Delete(fullPathToRecording);
743+
}
744+
}
745+
699746
#region SetRecordingOptions
700747
[Theory]
701748
[InlineData("{ \"HandleRedirects\": \"true\"}", true)]
@@ -977,17 +1024,41 @@ public IgnoreOnLinuxFact()
9771024
internal class MockHttpHandler : HttpMessageHandler
9781025
{
9791026
public const string DefaultResponse = "default response";
1027+
private readonly byte[] _responseContent;
1028+
private readonly string _contentType;
1029+
private readonly string _contentEncoding;
9801030

981-
public MockHttpHandler()
1031+
public MockHttpHandler(byte[] responseContent = default, string contentType = default, string contentEncoding = default)
9821032
{
1033+
_responseContent = responseContent ?? Encoding.UTF8.GetBytes(DefaultResponse);
1034+
_contentType = contentType ?? "application/json";
1035+
_contentEncoding = contentEncoding;
9831036
}
984-
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
1037+
1038+
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
9851039
{
1040+
// should not throw as stream should not be disposed
1041+
var content = await request.Content.ReadAsStringAsync();
1042+
Assert.NotEmpty(content);
1043+
var response = new HttpResponseMessage(HttpStatusCode.OK);
1044+
1045+
// we need to set the content before the content headers as otherwise they will be cleared out after setting content.
1046+
if (_contentEncoding == "gzip")
1047+
{
1048+
response.Content = new ByteArrayContent(GZipUtilities.CompressBodyCore(_responseContent));
1049+
}
1050+
else
1051+
{
1052+
response.Content = new ByteArrayContent(_responseContent);
1053+
}
9861054

987-
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
1055+
response.Content.Headers.ContentType = new MediaTypeHeaderValue(_contentType);
1056+
if (_contentEncoding != null)
9881057
{
989-
Content = new StringContent(DefaultResponse)
990-
});
1058+
response.Content.Headers.ContentEncoding.Add(_contentEncoding);
1059+
}
1060+
1061+
return response;
9911062
}
9921063
}
9931064
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"Entries": [
3+
{
4+
"RequestUri": "https://fakeazsdktestaccount.table.core.windows.net/Tables",
5+
"RequestMethod": "POST",
6+
"RequestHeaders": {
7+
"Accept": "application/json;odata=minimalmetadata",
8+
"Accept-Encoding": "gzip, deflate",
9+
"Authorization": "Sanitized",
10+
"Connection": "keep-alive",
11+
"Content-Length": "34",
12+
"Content-Type": "application/json",
13+
"Content-Encoding": "gzip",
14+
"DataServiceVersion": "3.0",
15+
"Date": "Tue, 18 May 2021 23:27:42 GMT",
16+
"User-Agent": "azsdk-python-data-tables/12.0.0b7 Python/3.8.6 (Windows-10-10.0.19041-SP0)",
17+
"x-ms-client-request-id": "a4c24b7a-b830-11eb-a05e-10e7c6392c5a",
18+
"x-ms-date": "Tue, 18 May 2021 23:27:42 GMT",
19+
"x-ms-version": "2019-02-02"
20+
},
21+
"RequestBody": "{\u0022TableName\u0022: \u0022listtable09bf2a3d\u0022}",
22+
"StatusCode": 201,
23+
"ResponseHeaders": {
24+
"Cache-Control": "no-cache",
25+
"Content-Type": "application/json",
26+
"Content-Encoding": "gzip",
27+
"Date": "Tue, 18 May 2021 23:27:43 GMT",
28+
"Retry-After": "10",
29+
"Location": "https://fakeazsdktestaccount.table.core.windows.net/Tables(\u0027listtable09bf2a3d\u0027)",
30+
"Server": [
31+
"Windows-Azure-Table/1.0",
32+
"Microsoft-HTTPAPI/2.0"
33+
],
34+
"Transfer-Encoding": "chunked",
35+
"X-Content-Type-Options": "nosniff",
36+
"x-ms-client-request-id": "a4c24b7a-b830-11eb-a05e-10e7c6392c5a",
37+
"x-ms-request-id": "d2270777-c002-0072-313d-4ce19f000000",
38+
"x-ms-version": "2019-02-02"
39+
},
40+
"ResponseBody": {
41+
"odata.metadata": "https://fakeazsdktestaccount.table.core.windows.net/$metadata#Tables/@Element",
42+
"TableName": "listtable09bf2a3d",
43+
"connectionString": null
44+
}
45+
}
46+
],
47+
"Variables": {}
48+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
using System.Collections.Generic;
2+
using System.IO;
3+
using System.IO.Compression;
4+
using System.Linq;
5+
using System.Net.Http.Headers;
6+
using Microsoft.AspNetCore.Http;
7+
8+
namespace Azure.Sdk.Tools.TestProxy.Common
9+
{
10+
/// <summary>
11+
/// Utility methods to compress and decompress content to/from GZip.
12+
/// </summary>
13+
public static class GZipUtilities
14+
{
15+
private const string Gzip = "gzip";
16+
private const string ContentEncoding = "Content-Encoding";
17+
18+
public static byte[] CompressBody(byte[] incomingBody, IDictionary<string, string[]> headers)
19+
{
20+
if (headers.TryGetValue(ContentEncoding, out var values) && values.Contains(Gzip))
21+
{
22+
return CompressBodyCore(incomingBody);
23+
}
24+
25+
return incomingBody;
26+
}
27+
28+
public static byte[] CompressBody(byte[] incomingBody, IHeaderDictionary headers)
29+
{
30+
if (headers.TryGetValue(ContentEncoding, out var values) && values.Contains(Gzip))
31+
{
32+
return CompressBodyCore(incomingBody);
33+
}
34+
35+
return incomingBody;
36+
}
37+
38+
public static byte[] CompressBodyCore(byte[] body)
39+
{
40+
using var uncompressedStream = new MemoryStream(body);
41+
using var resultStream = new MemoryStream();
42+
using (var compressedStream = new GZipStream(resultStream, CompressionMode.Compress))
43+
{
44+
uncompressedStream.CopyTo(compressedStream);
45+
}
46+
return resultStream.ToArray();
47+
}
48+
49+
public static byte[] DecompressBody(MemoryStream incomingBody, HttpContentHeaders headers)
50+
{
51+
if (headers.TryGetValues(ContentEncoding, out var values) && values.Contains(Gzip))
52+
{
53+
return DecompressBodyCore(incomingBody);
54+
}
55+
56+
return incomingBody.ToArray();
57+
}
58+
59+
public static byte[] DecompressBody(byte[] incomingBody, IHeaderDictionary headers)
60+
{
61+
if (headers.TryGetValue(ContentEncoding, out var values) && values.Contains(Gzip))
62+
{
63+
return DecompressBodyCore(new MemoryStream(incomingBody));
64+
}
65+
66+
return incomingBody;
67+
}
68+
69+
private static byte[] DecompressBodyCore(MemoryStream stream)
70+
{
71+
using var uncompressedStream = new GZipStream(stream, CompressionMode.Decompress);
72+
using var resultStream = new MemoryStream();
73+
uncompressedStream.CopyTo(resultStream);
74+
return resultStream.ToArray();
75+
}
76+
}
77+
}

tools/test-proxy/Azure.Sdk.Tools.TestProxy/Properties/launchSettings.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
"profiles": {
1111
"Azure.Sdk.Tools.TestProxy": {
1212
"commandName": "Project",
13-
"commandLineArgs": "--help",
1413
"environmentVariables": {
1514
"ASPNETCORE_ENVIRONMENT": "Development",
1615
"Logging__LogLevel__Microsoft": "Information"

tools/test-proxy/Azure.Sdk.Tools.TestProxy/RecordingHandler.cs

Lines changed: 7 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ public async Task HandleRecordRequestAsync(string recordingId, HttpRequest incom
200200

201201
var entry = await CreateEntryAsync(incomingRequest).ConfigureAwait(false);
202202

203-
var upstreamRequest = CreateUpstreamRequest(incomingRequest, entry.Request.Body);
203+
var upstreamRequest = CreateUpstreamRequest(incomingRequest, GZipUtilities.CompressBody(entry.Request.Body, entry.Request.Headers));
204204

205205
HttpResponseMessage upstreamResponse = null;
206206

@@ -218,7 +218,7 @@ public async Task HandleRecordRequestAsync(string recordingId, HttpRequest incom
218218
// HEAD requests do NOT have a body regardless of the value of the Content-Length header
219219
if (incomingRequest.Method.ToUpperInvariant() != "HEAD")
220220
{
221-
body = DecompressBody((MemoryStream)await upstreamResponse.Content.ReadAsStreamAsync().ConfigureAwait(false), upstreamResponse.Content.Headers);
221+
body = GZipUtilities.DecompressBody((MemoryStream)await upstreamResponse.Content.ReadAsStreamAsync().ConfigureAwait(false), upstreamResponse.Content.Headers);
222222
}
223223

224224
entry.Response.Body = body.Length == 0 ? null : body;
@@ -250,7 +250,7 @@ public async Task HandleRecordRequestAsync(string recordingId, HttpRequest incom
250250

251251
if (entry.Response.Body?.Length > 0)
252252
{
253-
var bodyData = CompressBody(entry.Response.Body, entry.Response.Headers);
253+
var bodyData = GZipUtilities.CompressBody(entry.Response.Body, entry.Response.Headers);
254254
outgoingResponse.ContentLength = bodyData.Length;
255255
await outgoingResponse.Body.WriteAsync(bodyData).ConfigureAwait(false);
256256
}
@@ -290,45 +290,6 @@ public static EntryRecordMode GetRecordMode(HttpRequest request)
290290
return mode;
291291
}
292292

293-
private byte[] CompressBody(byte[] incomingBody, SortedDictionary<string, string[]> headers)
294-
{
295-
if (headers.TryGetValue("Content-Encoding", out var values))
296-
{
297-
if (values.Contains("gzip"))
298-
{
299-
using (var uncompressedStream = new MemoryStream(incomingBody))
300-
using (var resultStream = new MemoryStream())
301-
{
302-
using (var compressedStream = new GZipStream(resultStream, CompressionMode.Compress))
303-
{
304-
uncompressedStream.CopyTo(compressedStream);
305-
}
306-
return resultStream.ToArray();
307-
}
308-
}
309-
}
310-
311-
return incomingBody;
312-
}
313-
314-
private byte[] DecompressBody(MemoryStream incomingBody, HttpContentHeaders headers)
315-
{
316-
if (headers.TryGetValues("Content-Encoding", out var values))
317-
{
318-
if (values.Contains("gzip"))
319-
{
320-
using (var uncompressedStream = new GZipStream(incomingBody, CompressionMode.Decompress))
321-
using (var resultStream = new MemoryStream())
322-
{
323-
uncompressedStream.CopyTo(resultStream);
324-
return resultStream.ToArray();
325-
}
326-
}
327-
}
328-
329-
return incomingBody.ToArray();
330-
}
331-
332293
public HttpRequestMessage CreateUpstreamRequest(HttpRequest incomingRequest, byte[] incomingBody)
333294
{
334295
var upstreamRequest = new HttpRequestMessage();
@@ -484,7 +445,7 @@ public async Task HandlePlaybackRequest(string recordingId, HttpRequest incoming
484445

485446
if (match.Response.Body?.Length > 0)
486447
{
487-
var bodyData = CompressBody(match.Response.Body, match.Response.Headers);
448+
var bodyData = GZipUtilities.CompressBody(match.Response.Body, match.Response.Headers);
488449

489450
outgoingResponse.ContentLength = bodyData.Length;
490451

@@ -506,7 +467,9 @@ public static async Task<RecordEntry> CreateEntryAsync(HttpRequest request)
506467
}
507468
}
508469

509-
entry.Request.Body = await ReadAllBytes(request.Body).ConfigureAwait(false);
470+
byte[] bytes = await ReadAllBytes(request.Body).ConfigureAwait(false);
471+
472+
entry.Request.Body = GZipUtilities.DecompressBody(bytes, request.Headers);
510473
return entry;
511474
}
512475

0 commit comments

Comments
 (0)