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

Add range support #6150

Merged
merged 3 commits into from
May 19, 2017
Merged

Add range support #6150

merged 3 commits into from
May 19, 2017

Conversation

jbagga
Copy link
Contributor

@jbagga jbagga commented Apr 17, 2017

Addresses #3702

This PR adds range support for FileContentResult and FileStreamResult

WIP for PhysicalFileResult and VirtualFileResult

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).

Copy link
Member

@javiercn javiercn left a 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;
Copy link
Member

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,
Copy link
Member

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)
Copy link
Member

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?

@jbagga
Copy link
Contributor Author

jbagga commented Apr 18, 2017

@javiercn Updated

Copy link
Member

@Tratcher Tratcher left a 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)
Copy link
Member

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)
Copy link
Member

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.

Copy link
Contributor Author

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

Copy link
Member

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);
Copy link
Member

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)
Copy link
Member

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; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nullable? This is optional

Copy link
Contributor Author

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.

Copy link
Member

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(
Copy link
Member

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;
Copy link
Member

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);
Copy link
Member

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)
Copy link
Member

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

@jbagga
Copy link
Contributor Author

jbagga commented Apr 19, 2017

Where should the If-Match and If-Modified-Since header be checked?

@Tratcher I didn't think these are specific to Range requests. Are they? They seem to be properties of the file.

@Tratcher
Copy link
Member

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.

@jbagga jbagga force-pushed the jbagga/ByteRange3702 branch from 2b058f8 to e16ae56 Compare April 20, 2017 01:46
@jbagga
Copy link
Contributor Author

jbagga commented Apr 20, 2017

@Tratcher Updated. Working on the sample and figuring out the end to end experience.

MediaTypeHeaderValue contentType,
bool enableRangeProcessing,
DateTimeOffset? lastModified,
EntityTagHeaderValue entityTag = null)
Copy link
Member

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);
Copy link
Member

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);
}

Copy link
Member

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);
Copy link
Member

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)
Copy link
Member

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;
Copy link
Member

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;
Copy link
Member

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)
Copy link
Member

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();
}

Copy link
Member

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;
Copy link
Member

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()
Copy link
Member

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);
Copy link
Member

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);
Copy link
Member

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

Copy link
Member

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)]
Copy link
Member

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?

Copy link
Member

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-

Copy link
Member

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).

Copy link
Contributor Author

@jbagga jbagga May 4, 2017

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?

@jbagga
Copy link
Contributor Author

jbagga commented Apr 26, 2017

@javiercn @Tratcher @kichalla Updated

}

// Internal for testing
internal static bool ComputeConditionalRequestHeaders(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Tratcher Could you review this one in particular? I tried to follow this


[Theory]
[InlineData(0, 4, "Hello", 5)]
[InlineData(6, 10, "World", 5)]
Copy link
Member

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);
Copy link
Member

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)
Copy link
Member

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)

Copy link
Contributor Author

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.

@jbagga
Copy link
Contributor Author

jbagga commented Apr 27, 2017

@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);
Copy link
Member

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

Copy link
Member

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?

Copy link
Contributor Author

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

Copy link
Contributor Author

@jbagga jbagga Apr 27, 2017

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

I don't like it either. All the other file types use StreamCopyOperation here. @Tratcher any other way to do this?

Copy link
Member

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

Copy link
Member

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.

Copy link
Contributor Author

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(
Copy link
Member

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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

var

return false;
}
}

Copy link
Member

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;
Copy link
Member

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)
Copy link
Member

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(
Copy link
Member

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);
Copy link
Member

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

Copy link
Contributor Author

@jbagga jbagga Apr 27, 2017

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();
Copy link
Member

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

Copy link
Contributor Author

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);
}

Copy link
Member

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"/>
Copy link
Member

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"/>.
Copy link
Member

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"/>.
Copy link
Member

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);
Copy link
Member

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

@jbagga
Copy link
Contributor Author

jbagga commented Apr 28, 2017

@javiercn @kichalla @Tratcher Updated

[NonAction]
public virtual FileStreamResult File(Stream fileStream, string contentType, string fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag)
{
return new FileStreamResult(fileStream, contentType, lastModified, entityTag) { FileDownloadName = fileDownloadName };
Copy link
Member

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)
Copy link
Member

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.

Copy link
Member

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.

Copy link
Contributor Author

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 ||
Copy link
Member

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()
Copy link
Member

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;
Copy link
Member

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.

Copy link
Contributor Author

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

Copy link
Member

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))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

skipped a set of { }

Copy link
Member

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);
Copy link
Member

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);
Copy link
Member

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)
Copy link
Member

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);
}

Copy link
Member

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);
Copy link
Member

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);
Copy link
Member

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>
Copy link
Member

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?

Copy link
Contributor Author

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

Copy link
Member

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?

Copy link
Contributor Author

@jbagga jbagga Apr 28, 2017

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)
Copy link
Member

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.

Copy link
Contributor Author

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

Copy link
Member

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.

Copy link
Contributor Author

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))
Copy link
Member

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(
Copy link
Member

@Tratcher Tratcher Apr 28, 2017

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?

@jbagga
Copy link
Contributor Author

jbagga commented Apr 28, 2017

@Tratcher Addressed most of the feedback. Need clarifications for other points.

@jbagga
Copy link
Contributor Author

jbagga commented Apr 28, 2017

@Tratcher Updated

{
if (request.Headers.ContainsKey(HeaderNames.Range))
{
if (preconditionState.Equals(PreconditionState.Unspecified) ||
Copy link
Member

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);
Copy link
Member

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)
Copy link
Member

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?

Copy link
Contributor Author

@jbagga jbagga May 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return max;
}

private (RangeItemHeaderValue range, long rangeLength) SetRangeHeaders(
Copy link
Member

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;
Copy link
Member

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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Don't set default values (they're the default!)
  2. Maybe make these properties just for fun? Or at least change the case so it doesn't conflict with the parameters below.

Copy link
Contributor Author

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;
Copy link
Member

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)]
Copy link
Member

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");
Copy link
Member

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");
Copy link
Member

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())
Copy link
Contributor

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);
}

Copy link
Contributor

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(
Copy link
Contributor

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; }
Copy link
Contributor

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(
Copy link
Contributor

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)
Copy link
Contributor

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(
Copy link
Contributor

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);
}

Copy link
Contributor

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);
}

Copy link
Contributor

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
Copy link
Contributor

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

@jbagga jbagga force-pushed the jbagga/ByteRange3702 branch from 9338778 to f7c5f68 Compare May 4, 2017 21:15
@jbagga
Copy link
Contributor Author

jbagga commented May 4, 2017

@Eilon @Tratcher Updated

Copy link
Member

@Tratcher Tratcher left a 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)
Copy link
Member

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?

Copy link
Member

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.

Copy link
Member

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)
Copy link
Member

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)
Copy link
Member

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);
Copy link
Member

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);
Copy link
Member

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();
Copy link
Member

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...

Copy link
Contributor Author

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?

Copy link
Member

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);
Copy link
Member

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.

Copy link
Member

@Tratcher Tratcher May 10, 2017

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?

Copy link
Contributor Author

@jbagga jbagga May 10, 2017

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)
Copy link
Member

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
Copy link
Member

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
Copy link
Member

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?

Copy link
Contributor Author

@jbagga jbagga May 9, 2017

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?

Copy link
Contributor Author

@jbagga jbagga May 10, 2017

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).

Copy link
Member

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.

Copy link
Member

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!

@jbagga jbagga force-pushed the jbagga/ByteRange3702 branch from f7c5f68 to 7edff12 Compare May 9, 2017 23:58
@jbagga
Copy link
Contributor Author

jbagga commented May 9, 2017

@Tratcher @Eilon Updated

{
httpResponseHeaders.ContentRange = new ContentRangeHeaderValue(fileLength.Value);
}
return (range: null, rangeLength: fileLength.Value, serveBody: false);
Copy link
Member

@Tratcher Tratcher May 10, 2017

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?

Copy link
Contributor Author

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

@jbagga
Copy link
Contributor Author

jbagga commented May 10, 2017

@Eilon Could you read over the documentation in ControllerBase?

@Tratcher Tratcher removed their assignment May 10, 2017
@jbagga jbagga force-pushed the jbagga/ByteRange3702 branch from e915231 to ead103e Compare May 11, 2017 19:27
@jbagga
Copy link
Contributor Author

jbagga commented May 11, 2017

@javiercn @kichalla @Tratcher @Eilon Updated. Please review or approve, thanks!

count: rangeLength,
cancellation: default(CancellationToken));
}
else
Copy link
Member

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?

@Tratcher Tratcher mentioned this pull request May 12, 2017
}

private static Task WriteFileAsync(ActionContext context, FileContentResult result)
private Task WriteFileAsync(ActionContext context, FileContentResult result, RangeItemHeaderValue range, long rangeLength)
Copy link
Member

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)
Copy link
Member

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)
Copy link
Member

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))
Copy link
Member

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)
Copy link
Member

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)
Copy link
Member

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?

Copy link
Contributor Author

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);
Copy link
Member

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;
Copy link
Member

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())
Copy link
Member

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

Copy link
Contributor Author

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(
Copy link
Member

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

Copy link
Member

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.

@jbagga jbagga force-pushed the jbagga/ByteRange3702 branch from 8415a44 to 249b6a4 Compare May 15, 2017 21:49
@jbagga
Copy link
Contributor Author

jbagga commented May 15, 2017

@javiercn @Eilon @kichalla Updated

@jbagga jbagga force-pushed the jbagga/ByteRange3702 branch from 249b6a4 to 09b894e Compare May 16, 2017 23:45
@jbagga jbagga force-pushed the jbagga/ByteRange3702 branch from c863a85 to d1a0b42 Compare May 18, 2017 22:02
@jbagga
Copy link
Contributor Author

jbagga commented May 18, 2017

@javiercn Added If-Range header tests
cc @Eilon

@jbagga jbagga merged commit 9aff0a6 into dev May 19, 2017
@jbagga jbagga deleted the jbagga/ByteRange3702 branch May 19, 2017 17:51
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants