Skip to content

Commit f0b44ac

Browse files
authored
Check Accept-Encoding headers before creating compression provider (#154)
1 parent 67c93a9 commit f0b44ac

File tree

7 files changed

+326
-52
lines changed

7 files changed

+326
-52
lines changed

src/Microsoft.AspNetCore.ResponseCompression/BodyWrapperStream.cs

Lines changed: 82 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,24 @@ namespace Microsoft.AspNetCore.ResponseCompression
1717
/// </summary>
1818
internal class BodyWrapperStream : Stream, IHttpBufferingFeature, IHttpSendFileFeature
1919
{
20-
private readonly HttpResponse _response;
20+
private readonly HttpContext _context;
2121
private readonly Stream _bodyOriginalStream;
2222
private readonly IResponseCompressionProvider _provider;
23-
private readonly ICompressionProvider _compressionProvider;
2423
private readonly IHttpBufferingFeature _innerBufferFeature;
2524
private readonly IHttpSendFileFeature _innerSendFileFeature;
2625

26+
private ICompressionProvider _compressionProvider = null;
2727
private bool _compressionChecked = false;
2828
private Stream _compressionStream = null;
29+
private bool _providerCreated = false;
30+
private bool _autoFlush = false;
2931

30-
internal BodyWrapperStream(HttpResponse response, Stream bodyOriginalStream, IResponseCompressionProvider provider, ICompressionProvider compressionProvider,
32+
internal BodyWrapperStream(HttpContext context, Stream bodyOriginalStream, IResponseCompressionProvider provider,
3133
IHttpBufferingFeature innerBufferFeature, IHttpSendFileFeature innerSendFileFeature)
3234
{
33-
_response = response;
35+
_context = context;
3436
_bodyOriginalStream = bodyOriginalStream;
3537
_provider = provider;
36-
_compressionProvider = compressionProvider;
3738
_innerBufferFeature = innerBufferFeature;
3839
_innerSendFileFeature = innerSendFileFeature;
3940
}
@@ -125,6 +126,10 @@ public override void Write(byte[] buffer, int offset, int count)
125126
if (_compressionStream != null)
126127
{
127128
_compressionStream.Write(buffer, offset, count);
129+
if (_autoFlush)
130+
{
131+
_compressionStream.Flush();
132+
}
128133
}
129134
else
130135
{
@@ -133,67 +138,101 @@ public override void Write(byte[] buffer, int offset, int count)
133138
}
134139

135140
#if NET451
136-
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
141+
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, Object state)
137142
{
138-
OnWrite();
139-
140-
if (_compressionStream != null)
141-
{
142-
return _compressionStream.BeginWrite(buffer, offset, count, callback, state);
143-
}
144-
return _bodyOriginalStream.BeginWrite(buffer, offset, count, callback, state);
143+
var tcs = new TaskCompletionSource<object>(state);
144+
InternalWriteAsync(buffer, offset, count, callback, tcs);
145+
return tcs.Task;
145146
}
146147

147-
public override void EndWrite(IAsyncResult asyncResult)
148+
private async void InternalWriteAsync(byte[] buffer, int offset, int count, AsyncCallback callback, TaskCompletionSource<object> tcs)
148149
{
149-
if (!_compressionChecked)
150+
try
150151
{
151-
throw new InvalidOperationException("BeginWrite was not called before EndWrite");
152+
await WriteAsync(buffer, offset, count);
153+
tcs.TrySetResult(null);
154+
}
155+
catch (Exception ex)
156+
{
157+
tcs.TrySetException(ex);
152158
}
153159

154-
if (_compressionStream != null)
160+
if (callback != null)
155161
{
156-
_compressionStream.EndWrite(asyncResult);
162+
// Offload callbacks to avoid stack dives on sync completions.
163+
var ignored = Task.Run(() =>
164+
{
165+
try
166+
{
167+
callback(tcs.Task);
168+
}
169+
catch (Exception)
170+
{
171+
// Suppress exceptions on background threads.
172+
}
173+
});
157174
}
158-
else
175+
}
176+
177+
public override void EndWrite(IAsyncResult asyncResult)
178+
{
179+
if (asyncResult == null)
159180
{
160-
_bodyOriginalStream.EndWrite(asyncResult);
181+
throw new ArgumentNullException(nameof(asyncResult));
161182
}
183+
184+
var task = (Task)asyncResult;
185+
task.GetAwaiter().GetResult();
162186
}
163187
#endif
164188

165-
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
189+
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
166190
{
167191
OnWrite();
168192

169193
if (_compressionStream != null)
170194
{
171-
return _compressionStream.WriteAsync(buffer, offset, count, cancellationToken);
195+
await _compressionStream.WriteAsync(buffer, offset, count, cancellationToken);
196+
if (_autoFlush)
197+
{
198+
await _compressionStream.FlushAsync(cancellationToken);
199+
}
200+
}
201+
else
202+
{
203+
await _bodyOriginalStream.WriteAsync(buffer, offset, count, cancellationToken);
172204
}
173-
return _bodyOriginalStream.WriteAsync(buffer, offset, count, cancellationToken);
174205
}
175206

176207
private void OnWrite()
177208
{
178209
if (!_compressionChecked)
179210
{
180211
_compressionChecked = true;
181-
182-
if (IsCompressable())
212+
if (_provider.ShouldCompressResponse(_context))
183213
{
184-
_response.Headers.Append(HeaderNames.ContentEncoding, _compressionProvider.EncodingName);
185-
_response.Headers.Remove(HeaderNames.ContentMD5); // Reset the MD5 because the content changed.
186-
_response.Headers.Remove(HeaderNames.ContentLength);
187-
188-
_compressionStream = _compressionProvider.CreateStream(_bodyOriginalStream);
214+
var compressionProvider = ResolveCompressionProvider();
215+
if (compressionProvider != null)
216+
{
217+
_context.Response.Headers.Append(HeaderNames.ContentEncoding, compressionProvider.EncodingName);
218+
_context.Response.Headers.Remove(HeaderNames.ContentMD5); // Reset the MD5 because the content changed.
219+
_context.Response.Headers.Remove(HeaderNames.ContentLength);
220+
221+
_compressionStream = compressionProvider.CreateStream(_bodyOriginalStream);
222+
}
189223
}
190224
}
191225
}
192226

193-
private bool IsCompressable()
227+
private ICompressionProvider ResolveCompressionProvider()
194228
{
195-
return !_response.Headers.ContainsKey(HeaderNames.ContentRange) && // The response is not partial
196-
_provider.ShouldCompressResponse(_response.HttpContext);
229+
if (!_providerCreated)
230+
{
231+
_providerCreated = true;
232+
_compressionProvider = _provider.GetCompressionProvider(_context);
233+
}
234+
235+
return _compressionProvider;
197236
}
198237

199238
public void DisableRequestBuffering()
@@ -205,13 +244,16 @@ public void DisableRequestBuffering()
205244
// For this to be effective it needs to be called before the first write.
206245
public void DisableResponseBuffering()
207246
{
208-
if (!_compressionProvider.SupportsFlush)
247+
if (ResolveCompressionProvider()?.SupportsFlush == false)
209248
{
210249
// Don't compress, some of the providers don't implement Flush (e.g. .NET 4.5.1 GZip/Deflate stream)
211250
// which would block real-time responses like SignalR.
212251
_compressionChecked = true;
213252
}
214-
253+
else
254+
{
255+
_autoFlush = true;
256+
}
215257
_innerBufferFeature?.DisableResponseBuffering();
216258
}
217259

@@ -257,6 +299,11 @@ private async Task InnerSendFileAsync(string path, long offset, long? count, Can
257299
{
258300
fileStream.Seek(offset, SeekOrigin.Begin);
259301
await StreamCopyOperation.CopyToAsync(fileStream, _compressionStream, count, cancellation);
302+
303+
if (_autoFlush)
304+
{
305+
await _compressionStream.FlushAsync(cancellation);
306+
}
260307
}
261308
}
262309
}

src/Microsoft.AspNetCore.ResponseCompression/IResponseCompressionProvider.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,12 @@ public interface IResponseCompressionProvider
2323
/// <param name="context"></param>
2424
/// <returns></returns>
2525
bool ShouldCompressResponse(HttpContext context);
26+
27+
/// <summary>
28+
/// Examines the request to see if compression should be used for response.
29+
/// </summary>
30+
/// <param name="context"></param>
31+
/// <returns></returns>
32+
bool CheckRequestAcceptsCompression(HttpContext context);
2633
}
2734
}

src/Microsoft.AspNetCore.ResponseCompression/Properties/AssemblyInfo.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33

44
using System.Reflection;
55
using System.Resources;
6+
using System.Runtime.CompilerServices;
7+
8+
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.ResponseCompression.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
69

710
[assembly: AssemblyMetadata("Serviceable", "True")]
811
[assembly: NeutralResourcesLanguage("en-us")]

src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionMiddleware.cs

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,13 @@ public class ResponseCompressionMiddleware
1818

1919
private readonly IResponseCompressionProvider _provider;
2020

21-
private readonly bool _enableForHttps;
2221

2322
/// <summary>
2423
/// Initialize the Response Compression middleware.
2524
/// </summary>
2625
/// <param name="next"></param>
2726
/// <param name="provider"></param>
28-
/// <param name="options"></param>
29-
public ResponseCompressionMiddleware(RequestDelegate next, IResponseCompressionProvider provider, IOptions<ResponseCompressionOptions> options)
27+
public ResponseCompressionMiddleware(RequestDelegate next, IResponseCompressionProvider provider)
3028
{
3129
if (next == null)
3230
{
@@ -36,14 +34,9 @@ public ResponseCompressionMiddleware(RequestDelegate next, IResponseCompressionP
3634
{
3735
throw new ArgumentNullException(nameof(provider));
3836
}
39-
if (options == null)
40-
{
41-
throw new ArgumentNullException(nameof(options));
42-
}
4337

4438
_next = next;
4539
_provider = provider;
46-
_enableForHttps = options.Value.EnableForHttps;
4740
}
4841

4942
/// <summary>
@@ -53,14 +46,7 @@ public ResponseCompressionMiddleware(RequestDelegate next, IResponseCompressionP
5346
/// <returns></returns>
5447
public async Task Invoke(HttpContext context)
5548
{
56-
ICompressionProvider compressionProvider = null;
57-
58-
if (!context.Request.IsHttps || _enableForHttps)
59-
{
60-
compressionProvider = _provider.GetCompressionProvider(context);
61-
}
62-
63-
if (compressionProvider == null)
49+
if (!_provider.CheckRequestAcceptsCompression(context))
6450
{
6551
await _next(context);
6652
return;
@@ -70,7 +56,7 @@ public async Task Invoke(HttpContext context)
7056
var originalBufferFeature = context.Features.Get<IHttpBufferingFeature>();
7157
var originalSendFileFeature = context.Features.Get<IHttpSendFileFeature>();
7258

73-
var bodyWrapperStream = new BodyWrapperStream(context.Response, bodyStream, _provider, compressionProvider,
59+
var bodyWrapperStream = new BodyWrapperStream(context, bodyStream, _provider,
7460
originalBufferFeature, originalSendFileFeature);
7561
context.Response.Body = bodyWrapperStream;
7662
context.Features.Set<IHttpBufferingFeature>(bodyWrapperStream);

src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionProvider.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public class ResponseCompressionProvider : IResponseCompressionProvider
1616
{
1717
private readonly ICompressionProvider[] _providers;
1818
private readonly HashSet<string> _mimeTypes;
19+
private readonly bool _enableForHttps;
1920

2021
/// <summary>
2122
/// If no compression providers are specified then GZip is used by default.
@@ -54,6 +55,8 @@ public ResponseCompressionProvider(IServiceProvider services, IOptions<ResponseC
5455
mimeTypes = ResponseCompressionDefaults.MimeTypes;
5556
}
5657
_mimeTypes = new HashSet<string>(mimeTypes, StringComparer.OrdinalIgnoreCase);
58+
59+
_enableForHttps = options.Value.EnableForHttps;
5760
}
5861

5962
/// <inheritdoc />
@@ -103,6 +106,11 @@ public virtual ICompressionProvider GetCompressionProvider(HttpContext context)
103106
/// <inheritdoc />
104107
public virtual bool ShouldCompressResponse(HttpContext context)
105108
{
109+
if (context.Response.Headers.ContainsKey(HeaderNames.ContentRange))
110+
{
111+
return false;
112+
}
113+
106114
var mimeType = context.Response.ContentType;
107115

108116
if (string.IsNullOrEmpty(mimeType))
@@ -121,5 +129,15 @@ public virtual bool ShouldCompressResponse(HttpContext context)
121129
// TODO PERF: StringSegments?
122130
return _mimeTypes.Contains(mimeType);
123131
}
132+
133+
/// <inheritdoc />
134+
public bool CheckRequestAcceptsCompression(HttpContext context)
135+
{
136+
if (context.Request.IsHttps && !_enableForHttps)
137+
{
138+
return false;
139+
}
140+
return !string.IsNullOrEmpty(context.Request.Headers[HeaderNames.AcceptEncoding]);
141+
}
124142
}
125143
}

0 commit comments

Comments
 (0)