Skip to content
This repository was archived by the owner on Dec 14, 2018. It is now read-only.

Do not serve response body for HEAD requests #7230

Merged
merged 2 commits into from
Jan 11, 2018
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 @@ -64,20 +64,16 @@ protected virtual (RangeItemHeaderValue range, long rangeLength, bool serveBody)
var response = context.HttpContext.Response;
SetLastModifiedAndEtagHeaders(response, lastModified, etag);

var serveBody = !HttpMethods.IsHead(request.Method);

// Short circuit if the preconditional headers process to 304 (NotModified) or 412 (PreconditionFailed)
if (preconditionState == PreconditionState.NotModified)
{
serveBody = false;
response.StatusCode = StatusCodes.Status304NotModified;
return (range: null, rangeLength: 0, serveBody);
return (range: null, rangeLength: 0, serveBody: false);
}
else if (preconditionState == PreconditionState.PreconditionFailed)
{
serveBody = false;
response.StatusCode = StatusCodes.Status412PreconditionFailed;
return (range: null, rangeLength: 0, serveBody);
return (range: null, rangeLength: 0, serveBody: false);
}

if (fileLength.HasValue)
Expand All @@ -86,10 +82,8 @@ protected virtual (RangeItemHeaderValue range, long rangeLength, bool serveBody)
// the length of the entire file.
// If the request is a valid range request, this header is overwritten with the length of the range as part of the
// range processing (see method SetContentLength).
if (serveBody)
{
response.ContentLength = fileLength.Value;
}

response.ContentLength = fileLength.Value;

// Handle range request
if (enableRangeProcessing)
Expand All @@ -111,7 +105,7 @@ protected virtual (RangeItemHeaderValue range, long rangeLength, bool serveBody)
}
}

return (range: null, rangeLength: 0, serveBody);
return (range: null, rangeLength: 0, serveBody: !HttpMethods.IsHead(request.Method));
}

private static void SetContentType(ActionContext context, FileResult result)
Expand Down Expand Up @@ -295,6 +289,7 @@ private static PreconditionState GetMaxPreconditionState(params PreconditionStat
{
var response = context.HttpContext.Response;
var httpResponseHeaders = response.GetTypedHeaders();
var serveBody = !HttpMethods.IsHead(context.HttpContext.Request.Method);

// Range may be null for empty range header, invalid ranges, parsing errors, multiple ranges
// and when the file length is zero.
Expand All @@ -306,7 +301,7 @@ private static PreconditionState GetMaxPreconditionState(params PreconditionStat

if (!isRangeRequest)
{
return (range: null, rangeLength: 0, serveBody: true);
return (range: null, rangeLength: 0, serveBody);
}

// Requested range is not satisfiable
Expand All @@ -330,7 +325,7 @@ private static PreconditionState GetMaxPreconditionState(params PreconditionStat
// Overwrite the Content-Length header for valid range requests with the range length.
var rangeLength = SetContentLength(response, range);

return (range, rangeLength, serveBody: true);
return (range, rangeLength, serveBody);
}

private static long SetContentLength(HttpResponse response, RangeItemHeaderValue range)
Expand Down
2 changes: 0 additions & 2 deletions test/Microsoft.AspNetCore.Mvc.Core.Test/FileResultTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,11 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Testing.xunit;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Testing;
using Microsoft.Net.Http.Headers;
using Moq;
using Xunit;
Expand Down
194 changes: 176 additions & 18 deletions test/Microsoft.AspNetCore.Mvc.FunctionalTests/FileResultTests.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Testing.xunit;
using Xunit;

Expand Down Expand Up @@ -189,11 +187,13 @@ public async Task FileFromDisk_ReturnsFileWithFileName()
Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
}

[Fact]
public async Task FileFromDisk_ReturnsFileWithFileName_RangeProcessingNotEnabled_RangeRequestedIgnored()
[Theory]
[InlineData("GET", "This is a sample text file")]
[InlineData("HEAD", "")]
public async Task FileFromDisk_ReturnsFileWithFileName_RangeProcessingNotEnabled_RangeRequestedIgnored(string httpMethod, string expectedBody)
{
// Arrange
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromDiskWithFileName");
var httpRequestMessage = new HttpRequestMessage(new HttpMethod(httpMethod), "http://localhost/DownloadFiles/DownloadFromDiskWithFileName");
httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6);

// Act
Expand All @@ -204,7 +204,7 @@ public async Task FileFromDisk_ReturnsFileWithFileName_RangeProcessingNotEnabled
Assert.NotNull(response.Content.Headers.ContentType);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
var body = await response.Content.ReadAsStringAsync();
Assert.Equal("This is a sample text file", body);
Assert.Equal(expectedBody, body);
}

[Fact]
Expand Down Expand Up @@ -246,6 +246,44 @@ public async Task FileFromDisk_ReturnsFileWithFileName_IfRangeHeaderInvalid_Rang
Assert.Equal("This is a sample text file", body);
}

[Theory]
[InlineData("", HttpStatusCode.OK, 26)]
[InlineData("bytes = 0-6", HttpStatusCode.PartialContent, 7)]
[InlineData("bytes = 17-25", HttpStatusCode.PartialContent, 9)]
[InlineData("bytes = 0-50", HttpStatusCode.PartialContent, 26)]
[InlineData("0-6", HttpStatusCode.OK, 26)]
[InlineData("bytes = ", HttpStatusCode.OK, 26)]
[InlineData("bytes = 1-4, 5-11", HttpStatusCode.OK, 26)]
[InlineData("bytes = 35-36", HttpStatusCode.RequestedRangeNotSatisfiable, 26)]
[InlineData("bytes = -0", HttpStatusCode.RequestedRangeNotSatisfiable, 26)]
public async Task FileFromDisk_ReturnsFileWithFileName_DoesNotServeBody_ForHeadRequest_WithLastModifiedAndEtag(string rangeString, HttpStatusCode httpStatusCode, int expectedContentLength)
{
// Arrange
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Head, "http://localhost/DownloadFiles/DownloadFromDiskWithFileName_WithLastModifiedAndEtag");
httpRequestMessage.Headers.TryAddWithoutValidation("Range", rangeString);
httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\""));

// Act
var response = await Client.SendAsync(httpRequestMessage);

// Assert
Assert.Equal(httpStatusCode, response.StatusCode);

Assert.NotNull(response.Content.Headers.ContentType);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());

var body = await response.Content.ReadAsStringAsync();
Assert.NotNull(body);
Assert.Equal(string.Empty, body);

var contentLength = response.Content.Headers.ContentLength;
Assert.Equal(expectedContentLength, contentLength);

var contentDisposition = response.Content.Headers.ContentDisposition.ToString();
Assert.NotNull(contentDisposition);
Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
}

[Fact]
public async Task FileFromStream_ReturnsFile()
{
Expand Down Expand Up @@ -347,11 +385,13 @@ public async Task FileFromStream_ReturnsFileWithFileName()
Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
}

[Fact]
public async Task FileFromStream_ReturnsFileWithFileName_RangeProcessingNotEnabled_RangeRequestedIgnored()
[Theory]
[InlineData("GET", "This is sample text from a stream")]
[InlineData("HEAD", "")]
public async Task FileFromStream_ReturnsFileWithFileName_RangeProcessingNotEnabled_RangeRequestedIgnored(string httpMethod, string expectedBody)
{
// Arrange
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromStreamWithFileName");
var httpRequestMessage = new HttpRequestMessage(new HttpMethod(httpMethod), "http://localhost/DownloadFiles/DownloadFromStreamWithFileName");
httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6);

// Act
Expand All @@ -362,7 +402,7 @@ public async Task FileFromStream_ReturnsFileWithFileName_RangeProcessingNotEnabl
Assert.NotNull(response.Content.Headers.ContentType);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
var body = await response.Content.ReadAsStringAsync();
Assert.Equal("This is sample text from a stream", body);
Assert.Equal(expectedBody, body);
}

[Fact]
Expand Down Expand Up @@ -402,6 +442,44 @@ public async Task FileFromStream_ReturnsFileWithFileName_IfRangeHeaderInvalid_Ra
Assert.Equal("This is sample text from a stream", body);
}

[Theory]
[InlineData("", HttpStatusCode.OK, 33)]
[InlineData("bytes = 0-6", HttpStatusCode.PartialContent, 7)]
[InlineData("bytes = 17-25", HttpStatusCode.PartialContent, 9)]
[InlineData("bytes = 0-50", HttpStatusCode.PartialContent, 33)]
[InlineData("0-6", HttpStatusCode.OK, 33)]
[InlineData("bytes = ", HttpStatusCode.OK, 33)]
[InlineData("bytes = 1-4, 5-11", HttpStatusCode.OK, 33)]
[InlineData("bytes = 35-36", HttpStatusCode.RequestedRangeNotSatisfiable, 33)]
[InlineData("bytes = -0", HttpStatusCode.RequestedRangeNotSatisfiable, 33)]
public async Task FileFromStream_ReturnsFileWithFileName_DoesNotServeBody_ForHeadRequest(string rangeString, HttpStatusCode httpStatusCode, int expectedContentLength)
{
// Arrange
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Head, "http://localhost/DownloadFiles/DownloadFromStreamWithFileName_WithEtag");
httpRequestMessage.Headers.TryAddWithoutValidation("Range", rangeString);
httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\""));

// Act
var response = await Client.SendAsync(httpRequestMessage);

// Assert
Assert.Equal(httpStatusCode, response.StatusCode);

Assert.NotNull(response.Content.Headers.ContentType);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());

var body = await response.Content.ReadAsStringAsync();
Assert.NotNull(body);
Assert.Equal(string.Empty, body);

var contentLength = response.Content.Headers.ContentLength;
Assert.Equal(expectedContentLength, contentLength);

var contentDisposition = response.Content.Headers.ContentDisposition.ToString();
Assert.NotNull(contentDisposition);
Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
}

[Fact]
public async Task FileFromBinaryData_ReturnsFile()
{
Expand Down Expand Up @@ -506,11 +584,13 @@ public async Task FileFromBinaryData_ReturnsFileWithFileName()
Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
}

[Fact]
public async Task FileFromBinaryData_ReturnsFileWithFileName_RangeProcessingNotEnabled_RangeRequestedIgnored()
[Theory]
[InlineData("GET", "This is a sample text from a binary array")]
[InlineData("HEAD", "")]
public async Task FileFromBinaryData_ReturnsFileWithFileName_RangeProcessingNotEnabled_RangeRequestedIgnored(string httpMethod, string expectedBody)
{
// Arrange
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromBinaryDataWithFileName");
var httpRequestMessage = new HttpRequestMessage(new HttpMethod(httpMethod), "http://localhost/DownloadFiles/DownloadFromBinaryDataWithFileName");
httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6);

// Act
Expand All @@ -521,7 +601,7 @@ public async Task FileFromBinaryData_ReturnsFileWithFileName_RangeProcessingNotE
Assert.NotNull(response.Content.Headers.ContentType);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
var body = await response.Content.ReadAsStringAsync();
Assert.Equal("This is a sample text from a binary array", body);
Assert.Equal(expectedBody, body);
}

[Fact]
Expand Down Expand Up @@ -563,6 +643,44 @@ public async Task FileFromBinaryData_ReturnsFileWithFileName_IfRangeHeaderInvali
Assert.Equal("This is a sample text from a binary array", body);
}

[Theory]
[InlineData("", HttpStatusCode.OK, 41)]
[InlineData("bytes = 0-6", HttpStatusCode.PartialContent, 7)]
[InlineData("bytes = 17-25", HttpStatusCode.PartialContent, 9)]
[InlineData("bytes = 0-50", HttpStatusCode.PartialContent, 41)]
[InlineData("0-6", HttpStatusCode.OK, 41)]
[InlineData("bytes = ", HttpStatusCode.OK, 41)]
[InlineData("bytes = 1-4, 5-11", HttpStatusCode.OK, 41)]
[InlineData("bytes = 45-46", HttpStatusCode.RequestedRangeNotSatisfiable, 41)]
[InlineData("bytes = -0", HttpStatusCode.RequestedRangeNotSatisfiable, 41)]
public async Task FileFromBinaryData_ReturnsFileWithFileName_DoesNotServeBody_ForHeadRequest(string rangeString, HttpStatusCode httpStatusCode, int expectedContentLength)
{
// Arrange
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Head, "http://localhost/DownloadFiles/DownloadFromBinaryDataWithFileName_WithEtag");
httpRequestMessage.Headers.TryAddWithoutValidation("Range", rangeString);
httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\""));

// Act
var response = await Client.SendAsync(httpRequestMessage);

// Assert
Assert.Equal(httpStatusCode, response.StatusCode);

Assert.NotNull(response.Content.Headers.ContentType);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());

var body = await response.Content.ReadAsStringAsync();
Assert.NotNull(body);
Assert.Equal(string.Empty, body);

var contentLength = response.Content.Headers.ContentLength;
Assert.Equal(expectedContentLength, contentLength);

var contentDisposition = response.Content.Headers.ContentDisposition.ToString();
Assert.NotNull(contentDisposition);
Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
}

[Fact]
public async Task FileFromEmbeddedResources_ReturnsFileWithFileName()
{
Expand Down Expand Up @@ -612,11 +730,13 @@ public async Task FileFromEmbeddedResources_ReturnsFileWithFileName_RangeRequest
Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
}

[Fact]
public async Task FileFromEmbeddedResources_ReturnsFileWithFileName_RangeProcessingNotEnabled_RangeRequestedIgnored()
[Theory]
[InlineData("GET", "Sample text file as embedded resource.")]
[InlineData("HEAD", "")]
public async Task FileFromEmbeddedResources_ReturnsFileWithFileName_RangeProcessingNotEnabled_RangeRequestedIgnored(string httpMethod, string expectedBody)
{
// Arrange
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/EmbeddedFiles/DownloadFileWithFileName_RangeProcessingNotEnabled");
var httpRequestMessage = new HttpRequestMessage(new HttpMethod(httpMethod), "http://localhost/EmbeddedFiles/DownloadFileWithFileName_RangeProcessingNotEnabled");
httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6);

// Act
Expand All @@ -627,7 +747,7 @@ public async Task FileFromEmbeddedResources_ReturnsFileWithFileName_RangeProcess
Assert.NotNull(response.Content.Headers.ContentType);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());
var body = await response.Content.ReadAsStringAsync();
Assert.Equal("Sample text file as embedded resource.", body);
Assert.Equal(expectedBody, body);
}

[Fact]
Expand Down Expand Up @@ -721,5 +841,43 @@ public async Task FileFromEmbeddedResources_ReturnsFileWithFileName_RangeRequest
Assert.NotNull(contentDisposition);
Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
}

[Theory]
[InlineData("", HttpStatusCode.OK, 38)]
[InlineData("bytes = 0-6", HttpStatusCode.PartialContent, 7)]
[InlineData("bytes = 17-25", HttpStatusCode.PartialContent, 9)]
[InlineData("bytes = 0-50", HttpStatusCode.PartialContent, 38)]
[InlineData("0-6", HttpStatusCode.OK, 38)]
[InlineData("bytes = ", HttpStatusCode.OK, 38)]
[InlineData("bytes = 1-4, 5-11", HttpStatusCode.OK, 38)]
[InlineData("bytes = 45-46", HttpStatusCode.RequestedRangeNotSatisfiable, 38)]
[InlineData("bytes = -0", HttpStatusCode.RequestedRangeNotSatisfiable, 38)]
public async Task FileFromEmbeddedResources_ReturnsFileWithFileName_DoesNotServeBody_ForHeadRequest(string rangeString, HttpStatusCode httpStatusCode, int expectedContentLength)
{
// Arrange
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Head, "http://localhost/EmbeddedFiles/DownloadFileWithFileName");
httpRequestMessage.Headers.TryAddWithoutValidation("Range", rangeString);
httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\""));

// Act
var response = await Client.SendAsync(httpRequestMessage);

// Assert
Assert.Equal(httpStatusCode, response.StatusCode);

Assert.NotNull(response.Content.Headers.ContentType);
Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString());

var body = await response.Content.ReadAsStringAsync();
Assert.NotNull(body);
Assert.Equal(string.Empty, body);

var contentLength = response.Content.Headers.ContentLength;
Assert.Equal(expectedContentLength, contentLength);

var contentDisposition = response.Content.Headers.ContentDisposition.ToString();
Assert.NotNull(contentDisposition);
Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition);
}
}
}