Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
using System.IO.Compression;
using System.Net;
using System.Text;
using System.Threading;
using EventStore.Transport.Http;
using EventStore.Transport.Http.EntityManagement;
using Microsoft.AspNetCore.Http;
using NUnit.Framework;

namespace EventStore.Core.Tests.Services.Transport.Http;
Expand Down Expand Up @@ -58,6 +60,70 @@ public void with_gzip_compression_algo_and_string_larger_than_50kb_data_is_gzipp
Assert.AreEqual(uncompressed, testString);
}

[Test]
public void with_gzip_compression_algo_empty_data_is_valid_gzip()
{
var response =
HttpEntityManager.CompressResponse(Array.Empty<byte>(), CompressionAlgorithms.Gzip);

Assert.Greater(response.Length, 0);

String uncompressed;

using (var inputStream = new MemoryStream(response))
using (var uncompressedStream = new GZipStream(inputStream, CompressionMode.Decompress))
using (var outputStream = new MemoryStream())
{
uncompressedStream.CopyTo(outputStream);
uncompressed = Encoding.UTF8.GetString(outputStream.ToArray());
}

Assert.AreEqual(uncompressed, string.Empty);
}

[Test]
public void empty_gzip_reply_has_a_valid_gzip_body()
{
AssertEmptyReplyHasValidBody(CompressionAlgorithms.Gzip, stream =>
new GZipStream(stream, CompressionMode.Decompress));
}

[Test]
public void empty_deflate_reply_has_a_valid_deflate_body()
{
AssertEmptyReplyHasValidBody(CompressionAlgorithms.Deflate, stream =>
new DeflateStream(stream, CompressionMode.Decompress));
}

private static void AssertEmptyReplyHasValidBody(string compressionAlgorithm, Func<Stream, Stream> decompress)
{
using var completed = new ManualResetEventSlim();
using var responseBody = new MemoryStream();
var context = new DefaultHttpContext();
context.Request.Scheme = "http";
context.Request.Host = new HostString("localhost");
context.Request.Path = "/";
context.Request.Headers["Accept-Encoding"] = compressionAlgorithm;
context.Response.Body = responseBody;
var entity = new HttpEntity(context, logHttpRequests: false, advertiseAsHost: null, advertiseAsPort: 0,
completed.Set);
var manager = entity.CreateManager();
Exception error = null;

manager.Reply(Array.Empty<byte>(), 200, "OK", "text/plain", Encoding.UTF8, null, ex => error = ex);

Assert.IsTrue(completed.Wait(TimeSpan.FromSeconds(5)));
Assert.IsNull(error);
Assert.AreEqual(compressionAlgorithm, context.Response.Headers["Content-Encoding"].ToString());
Assert.Greater(responseBody.Length, 0);

responseBody.Position = 0;
using var uncompressedStream = decompress(responseBody);
using var outputStream = new MemoryStream();
uncompressedStream.CopyTo(outputStream);
Assert.AreEqual(0, outputStream.Length);
}

[Test]
public void with_deflate_compression_algo_data_is_deflated()
{
Expand Down Expand Up @@ -101,6 +167,27 @@ public void with_deflate_compression_algo_and_string_larger_than_50kb_data_is_de
Assert.AreEqual(uncompressed, testString);
}

[Test]
public void with_deflate_compression_algo_empty_data_is_valid_deflate()
{
var response =
HttpEntityManager.CompressResponse(Array.Empty<byte>(), CompressionAlgorithms.Deflate);

Assert.Greater(response.Length, 0);

String uncompressed;

using (var inputStream = new MemoryStream(response))
using (var uncompressedStream = new DeflateStream(inputStream, CompressionMode.Decompress))
using (var outputStream = new MemoryStream())
{
uncompressedStream.CopyTo(outputStream);
uncompressed = Encoding.UTF8.GetString(outputStream.ToArray());
}

Assert.AreEqual(uncompressed, string.Empty);
}

[Test]
public void with_invalid_compression_algo_data_remains_the_same()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,30 @@ public bool IsProcessing
private static readonly string[] SupportedCompressionAlgorithms =
{CompressionAlgorithms.Gzip, CompressionAlgorithms.Deflate};

// .NET compression streams emit no payload until data is written, but clients still need a decodable member
// whenever a Content-Encoding header is advertised.
private static readonly byte[] EmptyGzipResponse =
{
0x1f, 0x8b,
0x08,
0x00,
0x00, 0x00, 0x00, 0x00,
0x00,
0xff,
0x01,
0x00, 0x00,
0xff, 0xff,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00
};

private static readonly byte[] EmptyDeflateResponse =
{
0x01,
0x00, 0x00,
0xff, 0xff
};

private static readonly BufferManager
_compressionBufferManager = new BufferManager(20, 50 * 1024); //create 20 50KB buffers (1MB total)

Expand Down Expand Up @@ -299,18 +323,19 @@ public void Reply(
if (!BeginReply(code, description, contentType, encoding, headers))
return;

LogResponse(response ?? Array.Empty<byte>());

if (!string.IsNullOrEmpty(_responseContentEncoding))
response = CompressResponse(response ?? Array.Empty<byte>(), _responseContentEncoding);

if (response == null || response.Length == 0)
{
LogResponse(Array.Empty<byte>());
SetResponseLength(0);
_onComplete();
CloseConnection(onError);
}
else
{
LogResponse(response);
if (!string.IsNullOrEmpty(_responseContentEncoding))
response = CompressResponse(response, _responseContentEncoding);
SetResponseLength(response.Length);
ContinueReply(response, onError, _onComplete);
}
Expand Down Expand Up @@ -502,6 +527,11 @@ public static byte[] CompressResponse(byte[] response, string compressionAlgorit
!SupportedCompressionAlgorithms.Contains(compressionAlgorithm))
return response;

if (response.Length == 0)
return compressionAlgorithm.Equals(CompressionAlgorithms.Gzip)
? EmptyGzipResponse.ToArray()
: EmptyDeflateResponse.ToArray();

MemoryStream outputStream;
var useBufferManager =
10L * response.Length <=
Expand Down
Loading