Skip to content

Commit cc61095

Browse files
andrewslavinTratcher
authored andcommitted
Support mime-type wildcards #121 @andrewslavin
1 parent e0be511 commit cc61095

File tree

4 files changed

+202
-7
lines changed

4 files changed

+202
-7
lines changed

samples/ResponseCompressionSample/Startup.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ public void ConfigureServices(IServiceCollection services)
2626
options.Providers.Add<CustomCompressionProvider>();
2727
// .Append(TItem) is only available on Core.
2828
options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[] { "image/svg+xml" });
29+
30+
////Example of using excluded and wildcard MIME types:
31+
////Compress all MIME types except various media types, but do compress SVG images.
32+
//options.MimeTypes = new[] { "*/*", "image/svg+xml" };
33+
//options.ExcludedMimeTypes = new[] { "image/*", "audio/*", "video/*" };
2934
});
3035
}
3136

src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionOptions.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,14 @@ public class ResponseCompressionOptions
1515
/// </summary>
1616
public IEnumerable<string> MimeTypes { get; set; }
1717

18+
/// <summary>
19+
/// Response Content-Type MIME types to not compress.
20+
/// </summary>
21+
public IEnumerable<string> ExcludedMimeTypes { get; set; }
22+
1823
/// <summary>
1924
/// Indicates if responses over HTTPS connections should be compressed. The default is 'false'.
20-
/// Enable compression on HTTPS connections may expose security problems.
25+
/// Enabling compression on HTTPS connections may expose security problems.
2126
/// </summary>
2227
public bool EnableForHttps { get; set; } = false;
2328

src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionProvider.cs

Lines changed: 44 additions & 6 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 HashSet<string> _excludedMimeTypes;
1920
private readonly bool _enableForHttps;
2021

2122
/// <summary>
@@ -34,7 +35,9 @@ public ResponseCompressionProvider(IServiceProvider services, IOptions<ResponseC
3435
throw new ArgumentNullException(nameof(options));
3536
}
3637

37-
_providers = options.Value.Providers.ToArray();
38+
var responseCompressionOptions = options.Value;
39+
40+
_providers = responseCompressionOptions.Providers.ToArray();
3841
if (_providers.Length == 0)
3942
{
4043
// Use the factory so it can resolve IOptions<GzipCompressionProviderOptions> from DI.
@@ -59,14 +62,19 @@ public ResponseCompressionProvider(IServiceProvider services, IOptions<ResponseC
5962
}
6063
}
6164

62-
var mimeTypes = options.Value.MimeTypes;
65+
var mimeTypes = responseCompressionOptions.MimeTypes;
6366
if (mimeTypes == null || !mimeTypes.Any())
6467
{
6568
mimeTypes = ResponseCompressionDefaults.MimeTypes;
6669
}
6770
_mimeTypes = new HashSet<string>(mimeTypes, StringComparer.OrdinalIgnoreCase);
6871

69-
_enableForHttps = options.Value.EnableForHttps;
72+
_excludedMimeTypes = new HashSet<string>(
73+
responseCompressionOptions.ExcludedMimeTypes ?? Enumerable.Empty<string>(),
74+
StringComparer.OrdinalIgnoreCase
75+
);
76+
77+
_enableForHttps = responseCompressionOptions.EnableForHttps;
7078
}
7179

7280
/// <inheritdoc />
@@ -115,7 +123,7 @@ public virtual ICompressionProvider GetCompressionProvider(HttpContext context)
115123
for (int i = 0; i < _providers.Length; i++)
116124
{
117125
var provider = _providers[i];
118-
126+
119127
// Any provider is a candidate.
120128
candidates.Add(new ProviderCandidate(provider.EncodingName, quality, i, provider));
121129
}
@@ -175,8 +183,9 @@ public virtual bool ShouldCompressResponse(HttpContext context)
175183
mimeType = mimeType.Trim();
176184
}
177185

178-
// TODO PERF: StringSegments?
179-
return _mimeTypes.Contains(mimeType);
186+
return ShouldCompressExact(mimeType) //check exact match type/subtype
187+
?? ShouldCompressPartial(mimeType) //check partial match type/*
188+
?? _mimeTypes.Contains("*/*"); //check wildcard */*
180189
}
181190

182191
/// <inheritdoc />
@@ -189,6 +198,35 @@ public bool CheckRequestAcceptsCompression(HttpContext context)
189198
return !string.IsNullOrEmpty(context.Request.Headers[HeaderNames.AcceptEncoding]);
190199
}
191200

201+
private bool? ShouldCompressExact(string mimeType)
202+
{
203+
//Check excluded MIME types first, then included
204+
if (_excludedMimeTypes.Contains(mimeType))
205+
{
206+
return false;
207+
}
208+
209+
if (_mimeTypes.Contains(mimeType))
210+
{
211+
return true;
212+
}
213+
214+
return null;
215+
}
216+
217+
private bool? ShouldCompressPartial(string mimeType)
218+
{
219+
int? slashPos = mimeType?.IndexOf('/');
220+
221+
if (slashPos >= 0)
222+
{
223+
string partialMimeType = mimeType.Substring(0, slashPos.Value) + "/*";
224+
return ShouldCompressExact(partialMimeType);
225+
}
226+
227+
return null;
228+
}
229+
192230
private readonly struct ProviderCandidate : IEquatable<ProviderCandidate>
193231
{
194232
public ProviderCandidate(string encodingName, double quality, int priority, ICompressionProvider provider)

test/Microsoft.AspNetCore.ResponseCompression.Tests/ResponseCompressionMiddlewareTest.cs

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,153 @@ public async Task MimeTypes_OtherContentTypes_NoMatch(string contentType)
224224
CheckResponseNotCompressed(response, expectedBodyLength: 100, sendVaryHeader: false);
225225
}
226226

227+
[Theory]
228+
[InlineData(null, null, "text/plain", true)]
229+
[InlineData(null, new string[0], "text/plain", true)]
230+
[InlineData(null, new[] { "TEXT/plain" }, "text/plain", false)]
231+
[InlineData(null, new[] { "TEXT/*" }, "text/plain", true)]
232+
[InlineData(null, new[] { "*/*" }, "text/plain", true)]
233+
234+
[InlineData(new string[0], null, "text/plain", true)]
235+
[InlineData(new string[0], new string[0], "text/plain", true)]
236+
[InlineData(new string[0], new[] { "TEXT/plain" }, "text/plain", false)]
237+
[InlineData(new string[0], new[] { "TEXT/*" }, "text/plain", true)]
238+
[InlineData(new string[0], new[] { "*/*" }, "text/plain", true)]
239+
240+
[InlineData(new[] { "TEXT/plain" }, null, "text/plain", true)]
241+
[InlineData(new[] { "TEXT/plain" }, new string[0], "text/plain", true)]
242+
[InlineData(new[] { "TEXT/plain" }, new[] { "TEXT/plain" }, "text/plain", false)]
243+
[InlineData(new[] { "TEXT/plain" }, new[] { "TEXT/*" }, "text/plain", true)]
244+
[InlineData(new[] { "TEXT/plain" }, new[] { "*/*" }, "text/plain", true)]
245+
246+
[InlineData(new[] { "TEXT/*" }, null, "text/plain", true)]
247+
[InlineData(new[] { "TEXT/*" }, new string[0], "text/plain", true)]
248+
[InlineData(new[] { "TEXT/*" }, new[] { "TEXT/plain" }, "text/plain", false)]
249+
[InlineData(new[] { "TEXT/*" }, new[] { "TEXT/*" }, "text/plain", false)]
250+
[InlineData(new[] { "TEXT/*" }, new[] { "*/*" }, "text/plain", true)]
251+
252+
[InlineData(new[] { "*/*" }, null, "text/plain", true)]
253+
[InlineData(new[] { "*/*" }, new string[0], "text/plain", true)]
254+
[InlineData(new[] { "*/*" }, new[] { "TEXT/plain" }, "text/plain", false)]
255+
[InlineData(new[] { "*/*" }, new[] { "TEXT/*" }, "text/plain", false)]
256+
[InlineData(new[] { "*/*" }, new[] { "*/*" }, "text/plain", true)]
257+
258+
[InlineData(null, null, "text/plain2", false)]
259+
[InlineData(null, new string[0], "text/plain2", false)]
260+
[InlineData(null, new[] { "TEXT/plain" }, "text/plain2", false)]
261+
[InlineData(null, new[] { "TEXT/*" }, "text/plain2", false)]
262+
[InlineData(null, new[] { "*/*" }, "text/plain2", false)]
263+
264+
[InlineData(new string[0], null, "text/plain2", false)]
265+
[InlineData(new string[0], new string[0], "text/plain2", false)]
266+
[InlineData(new string[0], new[] { "TEXT/plain" }, "text/plain2", false)]
267+
[InlineData(new string[0], new[] { "TEXT/*" }, "text/plain2", false)]
268+
[InlineData(new string[0], new[] { "*/*" }, "text/plain2", false)]
269+
270+
[InlineData(new[] { "TEXT/plain" }, null, "text/plain2", false)]
271+
[InlineData(new[] { "TEXT/plain" }, new string[0], "text/plain2", false)]
272+
[InlineData(new[] { "TEXT/plain" }, new[] { "TEXT/plain" }, "text/plain2", false)]
273+
[InlineData(new[] { "TEXT/plain" }, new[] { "TEXT/*" }, "text/plain2", false)]
274+
[InlineData(new[] { "TEXT/plain" }, new[] { "*/*" }, "text/plain2", false)]
275+
276+
[InlineData(new[] { "TEXT/*" }, null, "text/plain2", true)]
277+
[InlineData(new[] { "TEXT/*" }, new string[0], "text/plain2", true)]
278+
[InlineData(new[] { "TEXT/*" }, new[] { "TEXT/plain" }, "text/plain2", true)]
279+
[InlineData(new[] { "TEXT/*" }, new[] { "TEXT/*" }, "text/plain2", false)]
280+
[InlineData(new[] { "TEXT/*" }, new[] { "*/*" }, "text/plain2", true)]
281+
282+
[InlineData(new[] { "*/*" }, null, "text/plain2", true)]
283+
[InlineData(new[] { "*/*" }, new string[0], "text/plain2", true)]
284+
[InlineData(new[] { "*/*" }, new[] { "TEXT/plain" }, "text/plain2", true)]
285+
[InlineData(new[] { "*/*" }, new[] { "TEXT/*" }, "text/plain2", false)]
286+
[InlineData(new[] { "*/*" }, new[] { "*/*" }, "text/plain2", true)]
287+
public async Task MimeTypes_IncludedAndExcluded(
288+
string[] mimeTypes,
289+
string[] excludedMimeTypes,
290+
string mimeType,
291+
bool compress
292+
)
293+
{
294+
var builder = new WebHostBuilder()
295+
.ConfigureServices(
296+
services =>
297+
services.AddResponseCompression(
298+
options =>
299+
{
300+
options.MimeTypes = mimeTypes;
301+
options.ExcludedMimeTypes = excludedMimeTypes;
302+
}
303+
)
304+
)
305+
.Configure(
306+
app =>
307+
{
308+
app.UseResponseCompression();
309+
app.Run(
310+
context =>
311+
{
312+
context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
313+
context.Response.ContentType = mimeType;
314+
return context.Response.WriteAsync(new string('a', 100));
315+
}
316+
);
317+
}
318+
);
319+
320+
var server = new TestServer(builder);
321+
var client = server.CreateClient();
322+
323+
var request = new HttpRequestMessage(HttpMethod.Get, "");
324+
request.Headers.AcceptEncoding.ParseAdd("gzip");
325+
326+
var response = await client.SendAsync(request);
327+
328+
if (compress)
329+
{
330+
CheckResponseCompressed(response, expectedBodyLength: 24, expectedEncoding: "gzip");
331+
}
332+
else
333+
{
334+
CheckResponseNotCompressed(response, expectedBodyLength: 100, sendVaryHeader: false);
335+
}
336+
}
337+
338+
[Fact]
339+
public async Task NoIncludedMimeTypes_UseDefaults()
340+
{
341+
var builder = new WebHostBuilder()
342+
.ConfigureServices(
343+
services =>
344+
services.AddResponseCompression(
345+
options => options.ExcludedMimeTypes = new[] { "text/*" }
346+
)
347+
)
348+
.Configure(
349+
app =>
350+
{
351+
app.UseResponseCompression();
352+
app.Run(
353+
context =>
354+
{
355+
context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
356+
context.Response.ContentType = TextPlain;
357+
return context.Response.WriteAsync(new string('a', 100));
358+
}
359+
);
360+
}
361+
);
362+
363+
var server = new TestServer(builder);
364+
var client = server.CreateClient();
365+
366+
var request = new HttpRequestMessage(HttpMethod.Get, "");
367+
request.Headers.AcceptEncoding.ParseAdd("gzip");
368+
369+
var response = await client.SendAsync(request);
370+
371+
CheckResponseCompressed(response, expectedBodyLength: 24, expectedEncoding: "gzip");
372+
}
373+
227374
[Theory]
228375
[InlineData("")]
229376
[InlineData("text/plain")]

0 commit comments

Comments
 (0)