diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/FileResultExecutorBase.cs b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/FileResultExecutorBase.cs index bd66edfe3a..8c2ecb98a2 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/FileResultExecutorBase.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/FileResultExecutorBase.cs @@ -59,6 +59,14 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure var request = context.HttpContext.Request; var httpRequestHeaders = request.GetTypedHeaders(); + + // Since the 'Last-Modified' and other similar http date headers are rounded down to whole seconds, + // round down current file's last modified to whole seconds for correct comparison. + if (lastModified.HasValue) + { + lastModified = RoundDownToWholeSeconds(lastModified.Value); + } + var preconditionState = GetPreconditionState(httpRequestHeaders, lastModified, etag); var response = context.HttpContext.Response; @@ -219,7 +227,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure matchNotFoundState: PreconditionState.ShouldProcess); } - var now = DateTimeOffset.UtcNow; + var now = RoundDownToWholeSeconds(DateTimeOffset.UtcNow); // 14.25 If-Modified-Since var ifModifiedSince = httpRequestHeaders.IfModifiedSince; @@ -375,5 +383,11 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure } } } + + private static DateTimeOffset RoundDownToWholeSeconds(DateTimeOffset dateTimeOffset) + { + var ticksToRemove = dateTimeOffset.Ticks % TimeSpan.TicksPerSecond; + return dateTimeOffset.Subtract(TimeSpan.FromTicks(ticksToRemove)); + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/FileResultTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/FileResultTest.cs index 807f135930..da6808d055 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/FileResultTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/FileResultTest.cs @@ -396,6 +396,84 @@ namespace Microsoft.AspNetCore.Mvc Assert.Equal(expected, ifRangeIsValid); } + public static TheoryData LastModifiedDateData + { + get + { + return new TheoryData() + { + { new DateTimeOffset(2018, 4, 9, 11, 23, 22, TimeSpan.Zero), 200 }, + { new DateTimeOffset(2018, 4, 9, 11, 24, 21, TimeSpan.Zero), 200 }, + { new DateTimeOffset(2018, 4, 9, 11, 24, 22, TimeSpan.Zero), 304 }, + { new DateTimeOffset(2018, 4, 9, 11, 24, 23, TimeSpan.Zero), 304 }, + { new DateTimeOffset(2018, 4, 9, 11, 25, 22, TimeSpan.Zero), 304 }, + }; + } + } + + [Theory] + [MemberData(nameof(LastModifiedDateData))] + public async Task IfModifiedSinceComparison_OnlyUsesWholeSeconds( + DateTimeOffset ifModifiedSince, + int expectedStatusCode) + { + // Arrange + var httpContext = GetHttpContext(); + httpContext.Request.Headers[HeaderNames.IfModifiedSince] = HeaderUtilities.FormatDate(ifModifiedSince); + var actionContext = CreateActionContext(httpContext); + // Represents 4/9/2018 11:24:22 AM +00:00 + // Ticks rounded down to seconds: 636588698620000000 + var ticks = 636588698625969382; + var result = new EmptyFileResult("application/test") + { + LastModified = new DateTimeOffset(ticks, TimeSpan.Zero) + }; + + // Act + await result.ExecuteResultAsync(actionContext); + + // Assert + Assert.Equal(expectedStatusCode, httpContext.Response.StatusCode); + } + + public static TheoryData IfUnmodifiedSinceDateData + { + get + { + return new TheoryData() + { + { new DateTimeOffset(2018, 4, 9, 11, 23, 22, TimeSpan.Zero), 412 }, + { new DateTimeOffset(2018, 4, 9, 11, 24, 21, TimeSpan.Zero), 412 }, + { new DateTimeOffset(2018, 4, 9, 11, 24, 22, TimeSpan.Zero), 200 }, + { new DateTimeOffset(2018, 4, 9, 11, 24, 23, TimeSpan.Zero), 200 }, + { new DateTimeOffset(2018, 4, 9, 11, 25, 22, TimeSpan.Zero), 200 }, + }; + } + } + + [Theory] + [MemberData(nameof(IfUnmodifiedSinceDateData))] + public async Task IfUnmodifiedSinceComparison_OnlyUsesWholeSeconds(DateTimeOffset ifUnmodifiedSince, int expectedStatusCode) + { + // Arrange + var httpContext = GetHttpContext(); + httpContext.Request.Headers[HeaderNames.IfUnmodifiedSince] = HeaderUtilities.FormatDate(ifUnmodifiedSince); + var actionContext = CreateActionContext(httpContext); + // Represents 4/9/2018 11:24:22 AM +00:00 + // Ticks rounded down to seconds: 636588698620000000 + var ticks = 636588698625969382; + var result = new EmptyFileResult("application/test") + { + LastModified = new DateTimeOffset(ticks, TimeSpan.Zero) + }; + + // Act + await result.ExecuteResultAsync(actionContext); + + // Assert + Assert.Equal(expectedStatusCode, httpContext.Response.StatusCode); + } + private static IServiceCollection CreateServices() { var services = new ServiceCollection(); @@ -449,7 +527,12 @@ namespace Microsoft.AspNetCore.Mvc public Task ExecuteAsync(ActionContext context, EmptyFileResult result) { - SetHeadersAndLog(context, result, 0L, true); + SetHeadersAndLog( + context, + result, + fileLength: 0L, + enableRangeProcessing: true, + lastModified: result.LastModified); result.WasWriteFileCalled = true; return Task.FromResult(0); }