Skip to content

Commit b4f8e0f

Browse files
authored
ResponseCompression: set Content-Encoding and Vary headers for HEAD requests (#28464)
* ResponseCompression: set Content-Encoding and Vary headers for HEAD requests * Feedback
1 parent 9cfb133 commit b4f8e0f

File tree

2 files changed

+72
-30
lines changed

2 files changed

+72
-30
lines changed

src/Middleware/ResponseCompression/src/ResponseCompressionBody.cs

Lines changed: 41 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ internal async Task FinishCompressionAsync()
5959
{
6060
await _compressionStream.DisposeAsync();
6161
}
62+
63+
// Adds the compression headers for HEAD requests even if the body was not used.
64+
if (!_compressionChecked && HttpMethods.IsHead(_context.Request.Method))
65+
{
66+
InitializeCompressionHeaders();
67+
}
6268
}
6369

6470
HttpsCompressionMode IHttpsCompressionFeature.Mode { get; set; } = HttpsCompressionMode.Default;
@@ -232,40 +238,49 @@ public override async Task WriteAsync(byte[] buffer, int offset, int count, Canc
232238
}
233239
}
234240

235-
private void OnWrite()
241+
private void InitializeCompressionHeaders()
236242
{
237-
if (!_compressionChecked)
243+
if (_provider.ShouldCompressResponse(_context))
238244
{
239-
_compressionChecked = true;
240-
if (_provider.ShouldCompressResponse(_context))
241-
{
242-
// If the MIME type indicates that the response could be compressed, caches will need to vary by the Accept-Encoding header
243-
var varyValues = _context.Response.Headers.GetCommaSeparatedValues(HeaderNames.Vary);
244-
var varyByAcceptEncoding = false;
245+
// If the MIME type indicates that the response could be compressed, caches will need to vary by the Accept-Encoding header
246+
var varyValues = _context.Response.Headers.GetCommaSeparatedValues(HeaderNames.Vary);
247+
var varyByAcceptEncoding = false;
245248

246-
for (var i = 0; i < varyValues.Length; i++)
249+
for (var i = 0; i < varyValues.Length; i++)
250+
{
251+
if (string.Equals(varyValues[i], HeaderNames.AcceptEncoding, StringComparison.OrdinalIgnoreCase))
247252
{
248-
if (string.Equals(varyValues[i], HeaderNames.AcceptEncoding, StringComparison.OrdinalIgnoreCase))
249-
{
250-
varyByAcceptEncoding = true;
251-
break;
252-
}
253+
varyByAcceptEncoding = true;
254+
break;
253255
}
256+
}
254257

255-
if (!varyByAcceptEncoding)
256-
{
257-
_context.Response.Headers.Append(HeaderNames.Vary, HeaderNames.AcceptEncoding);
258-
}
258+
if (!varyByAcceptEncoding)
259+
{
260+
_context.Response.Headers.Append(HeaderNames.Vary, HeaderNames.AcceptEncoding);
261+
}
259262

260-
var compressionProvider = ResolveCompressionProvider();
261-
if (compressionProvider != null)
262-
{
263-
_context.Response.Headers.Append(HeaderNames.ContentEncoding, compressionProvider.EncodingName);
264-
_context.Response.Headers.Remove(HeaderNames.ContentMD5); // Reset the MD5 because the content changed.
265-
_context.Response.Headers.Remove(HeaderNames.ContentLength);
263+
var compressionProvider = ResolveCompressionProvider();
264+
if (compressionProvider != null)
265+
{
266+
_context.Response.Headers.Append(HeaderNames.ContentEncoding, compressionProvider.EncodingName);
267+
_context.Response.Headers.Remove(HeaderNames.ContentMD5); // Reset the MD5 because the content changed.
268+
_context.Response.Headers.Remove(HeaderNames.ContentLength);
269+
}
270+
}
271+
}
266272

267-
_compressionStream = compressionProvider.CreateStream(_innerStream);
268-
}
273+
private void OnWrite()
274+
{
275+
if (!_compressionChecked)
276+
{
277+
_compressionChecked = true;
278+
279+
InitializeCompressionHeaders();
280+
281+
if (_compressionProvider != null)
282+
{
283+
_compressionStream = _compressionProvider.CreateStream(_innerStream);
269284
}
270285
}
271286
}

src/Middleware/ResponseCompression/test/ResponseCompressionMiddlewareTest.cs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,25 @@ public async Task Request_AcceptUnknown_NotCompressed()
119119
AssertLog(logMessages.Skip(2).First(), LogLevel.Debug, "No matching response compression provider found.");
120120
}
121121

122+
[Fact]
123+
public async Task RequestHead_NoAcceptEncoding_Uncompressed()
124+
{
125+
var (response, logMessages) = await InvokeMiddleware(100, requestAcceptEncodings: null, responseType: TextPlain, httpMethod: HttpMethods.Head);
126+
127+
CheckResponseNotCompressed(response, expectedBodyLength: 100, sendVaryHeader: false);
128+
AssertLog(logMessages.Single(), LogLevel.Debug, "No response compression available, the Accept-Encoding header is missing or invalid.");
129+
}
130+
131+
[Fact]
132+
public async Task RequestHead_AcceptGzipDeflate_CompressedGzip()
133+
{
134+
var (response, logMessages) = await InvokeMiddleware(100, requestAcceptEncodings: new[] { "gzip", "deflate" }, responseType: TextPlain, httpMethod: HttpMethods.Head);
135+
136+
// Per RFC 7231, section 4.3.2, the Content-Lenght header can be omitted on HEAD requests.
137+
CheckResponseCompressed(response, expectedBodyLength: null, expectedEncoding: "gzip");
138+
AssertCompressedWithLog(logMessages, "gzip");
139+
}
140+
122141
[Theory]
123142
[InlineData("text/plain")]
124143
[InlineData("text/PLAIN")]
@@ -1154,7 +1173,8 @@ public async Task Dispose_SyncWriteOrFlushNotCalled(string encoding)
11541173
string[] requestAcceptEncodings,
11551174
string responseType,
11561175
Action<HttpResponse> addResponseAction = null,
1157-
Action<ResponseCompressionOptions> configure = null)
1176+
Action<ResponseCompressionOptions> configure = null,
1177+
string httpMethod = "GET")
11581178
{
11591179
var sink = new TestSink(
11601180
TestSink.EnableWithTypeName<ResponseCompressionProvider>,
@@ -1178,6 +1198,13 @@ public async Task Dispose_SyncWriteOrFlushNotCalled(string encoding)
11781198
{
11791199
context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
11801200
context.Response.ContentType = responseType;
1201+
1202+
if (HttpMethods.IsHead(context.Request.Method))
1203+
{
1204+
context.Response.ContentLength = uncompressedBodyLength;
1205+
return Task.CompletedTask;
1206+
}
1207+
11811208
addResponseAction?.Invoke(context.Response);
11821209
return context.Response.WriteAsync(new string('a', uncompressedBodyLength));
11831210
});
@@ -1189,7 +1216,7 @@ public async Task Dispose_SyncWriteOrFlushNotCalled(string encoding)
11891216
var server = host.GetTestServer();
11901217
var client = server.CreateClient();
11911218

1192-
var request = new HttpRequestMessage(HttpMethod.Get, "");
1219+
var request = new HttpRequestMessage(new HttpMethod(httpMethod), "");
11931220
for (var i = 0; i < requestAcceptEncodings?.Length; i++)
11941221
{
11951222
request.Headers.AcceptEncoding.Add(System.Net.Http.Headers.StringWithQualityHeaderValue.Parse(requestAcceptEncodings[i]));
@@ -1200,7 +1227,7 @@ public async Task Dispose_SyncWriteOrFlushNotCalled(string encoding)
12001227
return (response, sink.Writes.ToList());
12011228
}
12021229

1203-
private void CheckResponseCompressed(HttpResponseMessage response, int expectedBodyLength, string expectedEncoding)
1230+
private void CheckResponseCompressed(HttpResponseMessage response, long? expectedBodyLength, string expectedEncoding)
12041231
{
12051232
var containsVaryAcceptEncoding = false;
12061233
foreach (var value in response.Headers.GetValues(HeaderNames.Vary))
@@ -1217,7 +1244,7 @@ private void CheckResponseCompressed(HttpResponseMessage response, int expectedB
12171244
Assert.Equal(expectedBodyLength, response.Content.Headers.ContentLength);
12181245
}
12191246

1220-
private void CheckResponseNotCompressed(HttpResponseMessage response, int expectedBodyLength, bool sendVaryHeader)
1247+
private void CheckResponseNotCompressed(HttpResponseMessage response, long? expectedBodyLength, bool sendVaryHeader)
12211248
{
12221249
if (sendVaryHeader)
12231250
{

0 commit comments

Comments
 (0)