-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Conversation
b219722
to
9c55fd7
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not done, but here are a few things to improve
var response = httpContext.Response; | ||
|
||
var lastModified = result.LastModified; | ||
var etag = result.EntityTag; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I would just inline these variables given that you are already calling the method below within multiple lines.
var etag = result.EntityTag; | ||
|
||
var range = RangeHelper.ParseRange( | ||
context: httpContext, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid using named parameters here, they just add noise given that you have variables that explain what the parameter is for. I usually only use name parameters when passing literals, like null, true, "", 5, etc.
} | ||
|
||
private static Task WriteFileAsync(ActionContext context, FileContentResult result) | ||
private RangeItemHeaderValue SetContentRangeAndStatusCode(ActionContext context, FileContentResult result) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like this method is duped and not trivial, can you figure out a way to share this logic?
@javiercn Updated |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you expect the end-to-end scenario to look like? Where should the If-Match and If-Modified-Since header be checked?
https://github.com/aspnet/StaticFiles/blob/dev/src/Microsoft.AspNetCore.StaticFiles/StaticFileContext.cs#L167-L219
return response.Body.WriteAsync(result.FileContents, offset: 0, count: result.FileContents.Length); | ||
} | ||
|
||
else if (range == null || rangeLength == 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if no range was specified (as opposed to a range length of 0) then you should send the whole body.
return response.Body.WriteAsync(result.FileContents, offset: (int)range.From.Value, count: (int)rangeLength); | ||
} | ||
|
||
catch (OperationCanceledException ex) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you see this exception thrown? Kestrel and HttpSys don't throw this by default anymore, only if you pass in a CT.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I saw this in Static Files here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, it's passing a CT, you're not.
protected long SetRangeHeaders(ActionContext context, FileResult result, RangeItemHeaderValue range) | ||
{ | ||
var method = context.HttpContext.Request.Method; | ||
var isGet = string.Equals("GET", method, StringComparison.OrdinalIgnoreCase); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{ | ||
var method = context.HttpContext.Request.Method; | ||
var isGet = string.Equals("GET", method, StringComparison.OrdinalIgnoreCase); | ||
if (!isGet) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
or HEAD
/// <summary> | ||
/// Gets or sets the last modified information associated with the <see cref="FileStreamResult"/>. | ||
/// </summary> | ||
public DateTimeOffset LastModified { get; set; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nullable? This is optional
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay. What about EntityTag
? EntityTagHeaderValue
is not nullable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a class that can be null.
return range; | ||
} | ||
|
||
private ICollection<RangeItemHeaderValue> ParseRange( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't need to be a collection if you'll only ever return 0 or 1.
FileStream = fileStream; | ||
if (enableRangeProcessing) | ||
{ | ||
LastModified = lastModified; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These can be set regardless of range processing, they're just response headers
@@ -15,15 +17,54 @@ public FileContentResultExecutor(ILoggerFactory loggerFactory) | |||
|
|||
public Task ExecuteAsync(ActionContext context, FileContentResult result) | |||
{ | |||
RangeItemHeaderValue range = new RangeItemHeaderValue(0, 0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's strange to initialize this, just leave it null
var response = context.HttpContext.Response; | ||
var httpResponseHeaders = response.GetTypedHeaders(); | ||
bool rangeNotSatisfiable = false; | ||
if (ranges == null || ranges.Count != 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is overkill. It's not enough for ranges to be turned on, you must also do a soft-check for the right range headers and fall back to a normal response.
https://github.com/aspnet/StaticFiles/blob/dev/src/Microsoft.AspNetCore.StaticFiles/StaticFileMiddleware.cs#L103
https://github.com/aspnet/StaticFiles/blob/dev/src/Microsoft.AspNetCore.StaticFiles/StaticFileContext.cs#L221-L234
@Tratcher I didn't think these are specific to Range requests. Are they? They seem to be properties of the file. |
I guess not, they have to do with the etag and last modified you're adding. An end-to-end would need to account for them somewhere. |
2b058f8
to
e16ae56
Compare
@Tratcher Updated. Working on the sample and figuring out the end to end experience. |
MediaTypeHeaderValue contentType, | ||
bool enableRangeProcessing, | ||
DateTimeOffset? lastModified, | ||
EntityTagHeaderValue entityTag = null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't do default parameter values in public APIs in general (I believe)
@@ -15,15 +17,53 @@ public FileContentResultExecutor(ILoggerFactory loggerFactory) | |||
|
|||
public Task ExecuteAsync(ActionContext context, FileContentResult result) | |||
{ | |||
long rangeLength = default(long); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
var rangeLength = 0L; 😃
result.LastModified.Value, | ||
result.EntityTag); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: remove white-space
{ | ||
if (result.LastModified.HasValue) | ||
{ | ||
ComputeIfMatch(context, result, result.LastModified.Value, result.EntityTag); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove result.LastModified.Value from there, it's not being used inside the method
ActionContext context, | ||
long fileLength, | ||
DateTimeOffset? lastModified = null, | ||
EntityTagHeaderValue etag = null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No default parameter values :)
|
||
private static PreconditionState GetMaxPreconditionState(params PreconditionState[] states) | ||
{ | ||
PreconditionState max = PreconditionState.Unspecified; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
var
var range = ParseRange(context, fileLength, lastModified, etag); | ||
var response = context.HttpContext.Response; | ||
var httpResponseHeaders = response.GetTypedHeaders(); | ||
bool rangeNotSatisfiable = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rangeNotSatisfiable = range == null;
httpResponseHeaders.LastModified = lastModified; | ||
httpResponseHeaders.ETag = etag; | ||
|
||
if (rangeNotSatisfiable) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can just inline range == null here
var normalizedRanges = RangeHelper.NormalizeRanges(range, fileLength); | ||
return normalizedRanges.Single(); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: extra line
if (ifUnmodifiedSince.HasValue && ifUnmodifiedSince <= now) | ||
{ | ||
bool unmodified = ifUnmodifiedSince >= lastModified; | ||
_ifUnmodifiedSinceState = unmodified ? PreconditionState.ShouldProcess : PreconditionState.PreconditionFailed; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These classes are singletons, you can't add fields to them as one instance is shared across all requests of the app
} | ||
|
||
protected ILogger Logger { get; } | ||
|
||
public PreconditionState GetPreconditionState() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see this being used anywhere, so you can remove this, the public enum above and the GetMaxPreconditionState method below
entityTag: entityTag); | ||
|
||
var httpContext = GetHttpContext(); | ||
httpContext.Request.GetTypedHeaders().Range = new RangeHeaderValue(start, end); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a test where the request does not have this range header and the action says enableRangeProcessing
to true
?
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); | ||
|
||
// Act | ||
await result.ExecuteResultAsync(actionContext); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: move the following lines as part of Assert
. It would be cleaner to see the part which does the main act
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
?
|
||
[Theory] | ||
[InlineData(0, 4, "Hello", 5)] | ||
[InlineData(6, 10, "World", 5)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I recall there is a way to mention 'get me the content from 6th byte to the rest of the file' (without specifying the end index)...should we cover that scenario here or is it already covered in range helper tests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://tools.ietf.org/html/rfc7233#section-2.1 ..Look for the examples in these
Additional examples, assuming a representation of length 10000:
o The final 500 bytes (byte offsets 9500-9999, inclusive):
bytes=-500
Or:
bytes=9500-
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, interesting. My view on this is that we should do nothing more than the Static Files middleware already does (not that I know what it supports in this case).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Tratcher can answer if it is supported in StaticFiles but I believe it is. But I did add the support in this PR here: https://github.com/aspnet/Mvc/blob/jbagga/ByteRange3702/src/Microsoft.AspNetCore.Mvc.Core/Internal/FileResultExecutorBase.cs#L258-L265. Should I undo that?
e16ae56
to
926b169
Compare
} | ||
|
||
// Internal for testing | ||
internal static bool ComputeConditionalRequestHeaders( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
||
[Theory] | ||
[InlineData(0, 4, "Hello", 5)] | ||
[InlineData(6, 10, "World", 5)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://tools.ietf.org/html/rfc7233#section-2.1 ..Look for the examples in these
Additional examples, assuming a representation of length 10000:
o The final 500 bytes (byte offsets 9500-9999, inclusive):
bytes=-500
Or:
bytes=9500-
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); | ||
|
||
// Act | ||
await result.ExecuteResultAsync(actionContext); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
?
[InlineData("0-6")] | ||
[InlineData("bytes = 11-6")] | ||
[InlineData("bytes = 1-4, 5-11")] | ||
public async Task FileFromBinaryData_ReturnsFile_RangeRequestNotSatisfiable(string rangeString) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could have missed it, but where is the test where a client gets AcceptRanges header in the response even though they did not make a range request? (Like this is how a client can know that ranges are supported)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have this test. Is that what you mean? This is always set for all requests.
@kichalla Updated |
return response.Body.WriteAsync(result.FileContents, offset: 0, count: result.FileContents.Length); | ||
else | ||
{ | ||
return response.Body.WriteAsync(result.FileContents, offset: (int)range.From.Value, count: (int)rangeLength); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This casting from long to int looks dangerous. Wouldn't this break in the case you are sending a file bigger than 4GB? @Tratcher
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jbagga Do we check anywhere that the range is within the limits of the file?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A client can limit the number of bytes requested without knowing the
size of the selected representation. If the last-byte-pos value is
absent, or if the value is greater than or equal to the current
length of the representation data, the byte range is interpreted as
the remainder of the representation (i.e., the server replaces the
value of last-byte-pos with a value that is one less than the current
length of the selected representation).
From this which we do here in the NormalizeRange
method in the RangeHelper
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@javiercn in this case the user has to have 4GB of content in memory which is a highly unlikely scenario
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kichalla Not at all. For 1, .NET on 64 bits, 2 go to Azure or AWS and see how many machines you can buy that have gazillions of GB of memory.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have converted the byte array to a stream and used StreamCopyOperation
like other FileResult
types
} | ||
|
||
// Internal for testing | ||
internal static bool ComputeConditionalRequestHeaders( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe you are not computing anything here, consider changing the method name to something like ShouldProcessConditionalRequestHeaders or something similar
EntityTagHeaderValue etag = null) | ||
{ | ||
// 14.24 If-Match | ||
bool shouldProcess = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
var
return false; | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
extra empty line
var now = DateTimeOffset.UtcNow; | ||
if (ifUnmodifiedSince.HasValue && lastModified.HasValue && ifUnmodifiedSince <= now) | ||
{ | ||
bool unmodified = ifUnmodifiedSince >= lastModified; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
inline the variable
Stream fileStream, | ||
string contentType, | ||
DateTimeOffset? lastModified, | ||
EntityTagHeaderValue entityTag = null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no default parameters
var httpRequestHeaders = context.HttpContext.Request.GetTypedHeaders(); | ||
if (context.HttpContext.Request.Headers.ContainsKey(HeaderNames.Range)) | ||
{ | ||
bool shouldProcess = ComputeConditionalRequestHeaders( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method ComputeConditionalRequestHeaders does too much, it not just only computes the headers but in some cases it also writes the response. I would suggest you factor this in a different way.
Have a method that tells you what to do, e.g. tells you if you should process the request headers or if you should send back precondition failed or other type of response.
Then based on the result do what you need to do (set the headers, send a precondition failed response, or similar).
public FileResultExecutorBase(ILogger logger) | ||
{ | ||
Logger = logger; | ||
} | ||
|
||
protected ILogger Logger { get; } | ||
|
||
protected void SetHeadersAndLog(ActionContext context, FileResult result) | ||
protected (RangeItemHeaderValue range, long rangeLength)? SetHeadersAndLog(ActionContext context, FileResult result, long fileLength, DateTimeOffset? lastModified = null, EntityTagHeaderValue etag = null) | ||
{ | ||
SetContentType(context, result); | ||
SetContentDispositionHeader(context, result); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if you should send these headers in the cases where you are sending precondition failed or a similar response @Tratcher
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If precondition fails (412) or the response has not been modified (304), these are null and 0 respectively, which means the range will not be served, the entire file will be (which shouldn't happen so I am changing that right now). No other Range headers will be set either. @Tratcher do the Content-Range
and Content-Length
need to be set for these cases? It doesn't sound like it. https://tools.ietf.org/html/rfc7233#section-4.2
{ | ||
// Don't throw this exception, it's most likely caused by the client disconnecting. | ||
// However, if it was cancelled for any other reason we need to prevent empty responses. | ||
context.HttpContext.Abort(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I'm not sure why are we adding this code, this seems like a different feature/bug
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the try
block I am sending a CancellationToken
to StreamCopyOperation.CopyToAsync
. This is to catch the exception when the client disconnects.
fileStream.Seek(range.From.Value, SeekOrigin.Begin); | ||
await StreamCopyOperation.CopyToAsync(fileStream, response.Body, rangeLength, context.HttpContext.RequestAborted); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: empty line
/// </summary> | ||
/// <param name="fileContents">The bytes that represent the file contents.</param> | ||
/// <param name="contentType">The Content-Type header of the response.</param> | ||
/// <param name="lastModified">The <see cref="DateTimeOffset"/> of when the <see cref="FileContentResult"/> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: of when the <see cref="FileContentResult"/>
this should reference the parameter 'fileContents'. You can use paramref
} | ||
|
||
/// <summary> | ||
/// Gets or sets the last modified information associated with the <see cref="FileStreamResult"/>. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
with the <see cref="FileStreamResult"/>
=> should be FileContentResult
public DateTimeOffset? LastModified { get; set; } | ||
|
||
/// <summary> | ||
/// Gets or sets the etag associated with the <see cref="FileStreamResult"/>. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same here..update the comment
return response.Body.WriteAsync(result.FileContents, offset: 0, count: result.FileContents.Length); | ||
else | ||
{ | ||
return response.Body.WriteAsync(result.FileContents, offset: (int)range.From.Value, count: (int)rangeLength); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@javiercn in this case the user has to have 4GB of content in memory which is a highly unlikely scenario
[NonAction] | ||
public virtual FileStreamResult File(Stream fileStream, string contentType, string fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag) | ||
{ | ||
return new FileStreamResult(fileStream, contentType, lastModified, entityTag) { FileDownloadName = fileDownloadName }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why are lastModified and entityTag constructor parameters but FileDownloadName is not? It looks like the stream and content-type are required, but these new ones are optional so you don't need the new constructors.
return WriteFileAsync(context, result); | ||
RangeItemHeaderValue range; | ||
long rangeLength; | ||
if (result.LastModified.HasValue) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
e-tags and last-modified can be processed independently, you should check if either of them are set.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, you don't need this if-else, you can pass LastModified directly to SetHeadersAndLog because it accepts nullable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.Value throws if .HasValue is false. So I added a check to set it to null if .HasValue is false
} | ||
|
||
var statusCode = context.HttpContext.Response.StatusCode; | ||
if (statusCode == StatusCodes.Status412PreconditionFailed || |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This check is redundant. SetHeadersAndLog can return a bool from the same code that sets the status codes.
return (null, 0); | ||
} | ||
|
||
private void RequestMethodHead() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
?
@@ -18,18 +23,72 @@ public FileStreamResultExecutor(ILoggerFactory loggerFactory) | |||
|
|||
public Task ExecuteAsync(ActionContext context, FileStreamResult result) | |||
{ | |||
SetHeadersAndLog(context, result); | |||
return WriteFileAsync(context, result); | |||
var fileLength = 0L; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nullable? it won't be set for !CanSeek. It looks like this code sets Content-Length to 0 for non-seekable streams.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For non-seekable streams, calls to Length throw an exception. So I cannot get the length of the file. What's the expected behavior here? @Tratcher
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you can't get the length then you don't set content-length. Write the body normally and the server will set transfer-encoding: chunked.
You may not be able to calculate/verify ranges without the length,
if (context.HttpContext.Request.Headers.ContainsKey(HeaderNames.Range)) | ||
{ | ||
if (preconditionState.Equals(PreconditionState.Unspecified) || | ||
preconditionState.Equals(PreconditionState.ShouldProcess)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
skipped a set of { }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this nested if mess up your next else if HEAD?
{ | ||
var response = context.HttpContext.Response; | ||
var fileStream = GetFileStream(result.FileName); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why create the stream before the Path.IsPathRooted check?
{ | ||
var fileStream = GetFileStream(result.FileName); | ||
|
||
using (fileStream) | ||
{ | ||
await fileStream.CopyToAsync(response.Body, DefaultBufferSize); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use StreamCopyOperation here too, you want the same cancellation logic if the writes start failing.
{ | ||
var fileStream = GetFileStream(result.FileName); | ||
|
||
using (fileStream) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GetFileStream and using (fileStream) need to stay together. Otherwise you miss closing the stream in a few places. You moved GetFileStream to avoid duplication for the last else block, but you end up opening the stream when you didn't need to for the (rangeLength == 0) scenario and forgetting to close it. Copy the GetFileStream call to your last else block and add a using statement there too. Even better: using (var fileStream = GetFileStream(result.FileName))
fileStream.Seek(range.From.Value, SeekOrigin.Begin); | ||
await StreamCopyOperation.CopyToAsync(fileStream, response.Body, rangeLength, context.HttpContext.RequestAborted); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
empty line?
private async Task WriteFileAsync(ActionContext context, VirtualFileResult result, IFileInfo fileInfo, RangeItemHeaderValue range, long rangeLength) | ||
{ | ||
var response = context.HttpContext.Response; | ||
var fileStream = GetFileStream(fileInfo); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same problem, GetFileStream can't be separated from it's using statement.
{ | ||
var fileStream = GetFileStream(fileInfo); | ||
using (fileStream) | ||
{ | ||
await fileStream.CopyToAsync(response.Body, DefaultBufferSize); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
StreamCopyOperation
return new FileContentResult(fileContents, contentType, lastModified, entityTag) { FileDownloadName = fileDownloadName }; | ||
} | ||
|
||
/// <summary> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about etags for VirtualFileResult?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
VirtualFileResult and PhysicalFileResult have FileInfo with LastModified date. They don't have etags. Should it be a property like in FileContentResult and FileStreamResult? @Tratcher
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could Etag and LastModified properties go on the base FileResult?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it does, which LastModified will we look at? The one the user provides or the one from FileInfo? Or we only let users define the etag and not LastModified for Virtual and PhysicalFileResult? @Tratcher
@@ -59,14 +69,31 @@ public Task ExecuteAsync(ActionContext context, VirtualFileResult result) | |||
count: null, | |||
cancellation: default(CancellationToken)); | |||
} | |||
else | |||
else if (range == null || !fileStream.CanSeek) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even if you can't seek, you can still scan forward in the stream to get to a subrange.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How? If I set the count for a StreamCopyOperation to be the rangeLength but cannot set the beginning of the range to the range.From.Value, it wont be the correct range. @Tratcher
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It may be moot, I don't think you can do your proper range verification without knowing the length.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I spoke to @Eilon and we decided that we will just not support ranges for CanSeek = false. So I treat it like the range was not given and return the entire body
var fileProvider = GetFileProvider(result); | ||
|
||
var normalizedPath = result.FileName; | ||
if (normalizedPath.StartsWith("~", StringComparison.Ordinal)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sebastienros so ~ is meaningless for virtual files?
var fileInfo = GetFileInformation(result); | ||
RangeItemHeaderValue range; | ||
long rangeLength; | ||
(range, rangeLength) = SetHeadersAndLog( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need to confirm the file exists before setting headers. Move the FileNotFoundException all the way to the top?
@Tratcher Addressed most of the feedback. Need clarifications for other points. |
@Tratcher Updated |
{ | ||
if (request.Headers.ContainsKey(HeaderNames.Range)) | ||
{ | ||
if (preconditionState.Equals(PreconditionState.Unspecified) || |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can just use ==
to compare enum values.
} | ||
} | ||
|
||
return (null, 0, serveBody); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't recall with the new tuple syntax, but can you used the named values here? I.e. something like (range: null, rangeLength: 0, ...)
?
return (null, 0, serveBody); | ||
} | ||
|
||
private void SetContentType(ActionContext context, FileResult result) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could be made a static method? And the implementation can be just one line? context.HttpContext.Response.ContentType = result.ContentType;
? And at that point, is this helper needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not part of my change https://github.com/aspnet/Mvc/blob/dev/src/Microsoft.AspNetCore.Mvc.Core/Internal/FileResultExecutorBase.cs#L41. Should I still address the feedback?
return max; | ||
} | ||
|
||
private (RangeItemHeaderValue range, long rangeLength) SetRangeHeaders( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can some (or all?) of these utility methods be made static?
if (range != null) | ||
{ | ||
var rangeValue = range.Single(); | ||
long? from = rangeValue.From.HasValue ? rangeValue.From.Value : 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can do rangeValue.From ?? 0
(the "null-coalescing operator").
|
||
private class TestSendFileFeature : IHttpSendFileFeature | ||
{ | ||
public string name = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Don't set default values (they're the default!)
- Maybe make these properties just for fun? Or at least change the case so it doesn't conflict with the parameters below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got this from here Should I change that as well?
|
||
private class TestSendFileFeature : IHttpSendFileFeature | ||
{ | ||
public string name = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See earlier comment; apply everywhere.
@@ -37,6 +40,100 @@ public FileResultTests(MvcTestFixture<FilesWebSite.Startup> fixture) | |||
Assert.Equal("This is a sample text file", body); | |||
} | |||
|
|||
[ConditionalTheory] | |||
// https://github.com/aspnet/Mvc/issues/2727 | |||
[FrameworkSkipCondition(RuntimeFrameworks.Mono)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doubt this condition is still needed??
public IActionResult DownloadFromDisk_WithLastModifiedAndEtag() | ||
{ | ||
var path = Path.Combine(_hostingEnvironment.ContentRootPath, "sample.txt"); | ||
var lastModified = DateTimeOffset.Parse("05/01/2008 +1:00"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DTO.Parse() uses the current culture, so this will not have the desired behavior on some machines. Either use ParseExact, or call a DTO ctor that takes in explicit year/month/day/time/etc. integers, e.g. https://msdn.microsoft.com/en-us/library/bb354978(v=vs.110).aspx
public IActionResult DownloadFromDiskWithFileName_WithLastModifiedAndEtag() | ||
{ | ||
var path = Path.Combine(_hostingEnvironment.ContentRootPath, "sample.txt"); | ||
var lastModified = DateTimeOffset.Parse("05/01/2008 +1:00"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See previous comment.
string contentType, | ||
DateTimeOffset? lastModified, | ||
EntityTagHeaderValue entityTag = null) | ||
: base(contentType?.ToString()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the ToString()
?
{ | ||
return response.Body.WriteAsync(result.FileContents, offset: 0, count: result.FileContents.Length); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extra line
var httpRequestHeaders = context.HttpContext.Request.GetTypedHeaders(); | ||
if (context.HttpContext.Request.Headers.ContainsKey(HeaderNames.Range)) | ||
{ | ||
bool shouldProcess = ComputeConditionalRequestHeaders( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
var
all day every day
/// </summary> | ||
public EntityTagHeaderValue EntityTag | ||
{ | ||
get { return _entityTag ?? null; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems odd
///// <param name="lastModified">The <see cref="DateTimeOffset"/> of when the <paramref name="fileContents"/> | ||
///// was last modified.</param> | ||
///// <param name="entityTag">The entity tag associated with the <paramref name="fileContents"/>.</param> | ||
//public FileContentResult( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No commented code
} | ||
|
||
private static Task WriteFileAsync(ActionContext context, FileContentResult result) | ||
private static async Task WriteFileAsync(ActionContext context, FileContentResult result, RangeItemHeaderValue range, long rangeLength) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be able to just return it (rather than awaiting the Task)
} | ||
|
||
// Internal for testing | ||
internal static PreconditionState CheckPreconditionHeaders( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GetPreconditionState
\ CalculatePreconditionState
?
fileStream.Seek(range.From.Value, SeekOrigin.Begin); | ||
await StreamCopyOperation.CopyToAsync(fileStream, outputStream, rangeLength, BufferSize, context.RequestAborted); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extra line
{ | ||
await StreamCopyOperation.CopyToAsync(fileStream, outputStream, null, BufferSize, context.RequestAborted); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extra line
}; | ||
} | ||
|
||
protected class FileInfo |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FileMetadata
? PhysicalFileInfo
is also a type from our libraries
9338778
to
f7c5f68
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Almost done. More questions and style than anything else.
protected ILogger Logger { get; } | ||
|
||
protected void SetHeadersAndLog(ActionContext context, FileResult result) | ||
protected (RangeItemHeaderValue range, long rangeLength, bool serveBody) SetHeadersAndLog(ActionContext context, FileResult result, long? fileLength, DateTimeOffset? lastModified = null, EntityTagHeaderValue etag = null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Eilon are you OK with using tuples on public (or protected) APIs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Eilon? It's on the internal namespace so I think its OK, but want to get your input.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Formatting: This is impossible to read, format it as follows:
protected (RangeItemHeaderValue range, long rangeLength, bool serveBody) SetHeadersAndLog(
ActionContext context,
FileResult result,
....)
{
...
}
In general stick to less than 120 characters per line
{ | ||
var response = context.HttpContext.Response; | ||
var outputStream = response.Body; | ||
var fileContentsStream = new MemoryStream(result.FileContents); | ||
if (range != null && rangeLength == 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why isn't this check first?
preconditionState == PreconditionState.ShouldProcess) | ||
{ | ||
var rangeInfo = SetRangeHeaders(context, httpRequestHeaders, fileLength, lastModified, etag); | ||
if (response.StatusCode == StatusCodes.Status416RangeNotSatisfiable) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should check a bool returned from SetRangeHeaders (e.g. serveBody?) rather than inspecting the response object for side-effects.
{ | ||
var response = context.HttpContext.Response; | ||
var httpResponseHeaders = response.GetTypedHeaders(); | ||
var range = fileLength == null ? null : ParseRange(context, httpRequestHeaders, fileLength.Value, lastModified, etag); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fileLength.HasValue
{ | ||
var response = context.HttpContext.Response; | ||
var httpResponseHeaders = response.GetTypedHeaders(); | ||
var range = fileLength == null ? null : ParseRange(context, httpRequestHeaders, fileLength.Value, lastModified, etag); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's worth commenting that you explicitly checked for the presence of the range header prior to calling this method so you'd only end up with range == null for a parsing error (or the missing file length)
|
||
if (range != null) | ||
{ | ||
var rangeValue = range.Single(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering why ParseRange returned a collection when it repeatedly restricts it to a single range...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's how the helper is set up. RangeHelper.ParseRange returns a collection of Ranges from the range header. Should we revisit the helper?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can clean it up later.
} | ||
else | ||
{ | ||
fileStream.Seek(range.From.Value, SeekOrigin.Begin); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you confirmed this stream is seekable on all code paths? I only see one new check for CanSeek.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't ask you to add another CanSeek check on 295, that's too late, you've already set headers. I asked if you had made sure that all streams that can be passed into WriteFileAsync are seekable. Check the call sites and see where those streams are coming from. E.g. MemoryStream and FileStream should be fine, but I don't think you have the same guarantee from VirtualFile?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For all file types except the FileStream
, the stream is retrieved right before I call this method (in order to write to the response body). If I want to not set the headers for CanSeek = false streams, I should move the code to get the stream in ExecuteAsync
before calling SetHeadersAndLog
, check for CanSeek and if true send that in to the WriteFileAsync
SetHeadersAndLog(context, result); | ||
return WriteFileAsync(context, result); | ||
var fileInfo = GetFileInfo(result.FileName); | ||
if (fileInfo.Exists) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: the nesting could be simplified by checking if (!fileInfo.Exists)
|
||
var fileInfo = fileProvider.GetFileInfo(normalizedPath); | ||
if (fileInfo.Exists) | ||
else |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: no else required after throwing
} | ||
|
||
/// <summary> | ||
/// Returns a file with the specified range in the given <paramref name="fileContents" /> as content (<see cref="StatusCodes.Status206PartialContent"/> or |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ranges are not the point of the API (nor even a parameter to the API), they're an implementation detail. How about just putting something at the end of the summary that says "This supports range requests." and then we do a docs writeup for the details?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If lastModified and etag are not specified, the PreconditionState
is Unspecified
and the range may still be served if the Range header is correctly provided. So that additional comment for
"This supports range requests."
is relevant for all file controller actions in ControllerBase
.
Also will it be useful to also mention the possible StatusCodes to expect (206 or 416) as part of the comment?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Tratcher Example summary:
/// Returns a file in the specified <paramref name="fileStream" /> (<see cref="StatusCodes.Status200OK"/>),
/// and the specified <paramref name="contentType" /> as the Content-Type.
/// This supports range requests (<see cref="StatusCodes.Status206PartialContent"/> or
/// <see cref="StatusCodes.Status416RangeNotSatisfiable"/> if the range is not satisfiable).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better. I'll defer to @Eilon for MVC comment style and depth.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah that looks great to me!
f7c5f68
to
7edff12
Compare
{ | ||
httpResponseHeaders.ContentRange = new ContentRangeHeaderValue(fileLength.Value); | ||
} | ||
return (range: null, rangeLength: fileLength.Value, serveBody: false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isn't range length 0 / ignored if range is null?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes it is ignored essentially since this sets serveBody to false and returns from ExecuteAsync without writing to the response body
@Eilon Could you read over the documentation in |
e915231
to
ead103e
Compare
count: rangeLength, | ||
cancellation: default(CancellationToken)); | ||
} | ||
else |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: flatten redundant else's?
} | ||
|
||
private static Task WriteFileAsync(ActionContext context, FileContentResult result) | ||
private Task WriteFileAsync(ActionContext context, FileContentResult result, RangeItemHeaderValue range, long rangeLength) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this not static anymore?
protected ILogger Logger { get; } | ||
|
||
protected void SetHeadersAndLog(ActionContext context, FileResult result) | ||
protected (RangeItemHeaderValue range, long rangeLength, bool serveBody) SetHeadersAndLog(ActionContext context, FileResult result, long? fileLength, DateTimeOffset? lastModified = null, EntityTagHeaderValue etag = null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Eilon? It's on the internal namespace so I think its OK, but want to get your input.
protected ILogger Logger { get; } | ||
|
||
protected void SetHeadersAndLog(ActionContext context, FileResult result) | ||
protected (RangeItemHeaderValue range, long rangeLength, bool serveBody) SetHeadersAndLog(ActionContext context, FileResult result, long? fileLength, DateTimeOffset? lastModified = null, EntityTagHeaderValue etag = null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Formatting: This is impossible to read, format it as follows:
protected (RangeItemHeaderValue range, long rangeLength, bool serveBody) SetHeadersAndLog(
ActionContext context,
FileResult result,
....)
{
...
}
In general stick to less than 120 characters per line
serveBody = false; | ||
} | ||
|
||
if (request.Headers.ContainsKey(HeaderNames.Range)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
request.Headers.ContainsKey(HeaderNames.Range) &&
(preconditionState == PreconditionState.Unspecified || preconditionState == PreconditionState.ShouldProcess))
// Range may be null for parsing errors, multiple ranges and when the file length is missing. | ||
var range = fileLength.HasValue ? ParseRange(context, httpRequestHeaders, fileLength.Value, lastModified, etag) : null; | ||
var rangeNotSatisfiable = range == null; | ||
if (rangeNotSatisfiable) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inline this variable
|
||
// 14.25 If-Modified-Since | ||
var ifModifiedSince = httpRequestHeaders.IfModifiedSince; | ||
if (ifModifiedSince.HasValue && ifModifiedSince <= now) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you need ifModifiedSince <= now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If ifModifiedSince > now, that's an invalid ifModifiedSince
value
ifUnmodifiedSinceState = unmodified ? PreconditionState.ShouldProcess : PreconditionState.PreconditionFailed; | ||
} | ||
|
||
var state = GetMaxPreconditionState(ifMatchState, ifNoneMatchState, ifModifiedSinceState, ifUnmodifiedSinceState); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand why this code gives preference to PreconditionFailed over NotModified. This computation can be done in place when processing each of the individual headers.
} | ||
|
||
var preconditionState = GetPreconditionState(context, httpRequestHeaders, lastModified, etag); | ||
var serveBody = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be simplified to serveBody = !HttpMethods.IsHead(request.Method)
|
||
// 14.24 If-Match | ||
var ifMatch = httpRequestHeaders.IfMatch; | ||
if (ifMatch != null && ifMatch.Any()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can extract this block of code into a method and reuse it below, something like:
EtagMatches(ifMatch,etag,matchingResult,notMatchingResult){
if (ifMatch != null && ifMatch.Any())
{
ifMatchState = notMachingResult;
foreach (var entityTag in ifMatch)
{
if (entityTag.Equals(EntityTagHeaderValue.Any) || entityTag.Compare(etag, useStrongComparison: true))
{
ifMatchState = matchingResult;
break;
}
}
}
}
The same technique can be applied to the ifModifiedSince
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For checking lastModified, the checks are trivial so I am not extracting out the method. I did for etag matching though
} | ||
|
||
// Internal for testing | ||
internal static PreconditionState GetPreconditionState( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we do the checks in the following order? https://tools.ietf.org/html/rfc7232#section-6
@Tratcher
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, that RFC was released about the time the original StaticFiles code was written...
Giving PreconditionFailed the highest priority has almost the same effect as that stated ordering.
8415a44
to
249b6a4
Compare
249b6a4
to
09b894e
Compare
c863a85
to
d1a0b42
Compare
Addresses #3702
This PR adds range support for
FileContentResult
andFileStreamResult
WIP for
PhysicalFileResult
andVirtualFileResult
cc @pranavkm @Tratcher @javiercn I'd like some feedback on how to set this up. Right now there is a lot of back and forth between the base executor and the specific executors (mostly because the base executor has the code common for every file type).