The problem today is that when using WebApi 2 and an Async ApiController - based Get method, that is returning the contents of a file. When I change the Get method to synchronous, it works just fine, but as soon as I convert it back to async, it closes the stream prematurely. (Fiddler reports the connection was aborted) The working Synchronous code is:
public void Get(int id)
{
try
{
FileInfo fileInfo = logic.GetFileInfoSync(id);
HttpResponse response = HttpContext.Current.Response;
response.Clear();
response.ClearContent();
response.Buffer = true;
response.AddHeader("Content-Disposition", "attachment; filename=\"" + fileInfo.Node.Name + fileInfo.Ext + "\"");
response.AddHeader("Content-Length", fileInfo.SizeInBytes.ToString());
response.ContentType = "application/octet-stream";
logic.GetDownloadStreamSync(id, response.OutputStream);
response.StatusCode = (int)HttpStatusCode.OK;
//HttpContext.Current.ApplicationInstance.CompleteRequest();
response.End();
}
catch(Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
And the GetDownloadStreamSync is as follows:
public async Task GetDownloadStream(string fileIdentifier, Stream streamToCopyTo)
{
string filePath = Path.Combine(fileIdentifierFolder, fileIdentifier);
using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.None, BufferSize, false))
{
fs.CopyTo(streamToCopyTo);
}
}
--------Async Code ----------
The Async version is exact same except:
public async Task Get(int id)
{
FileInfo fileInfo = await logic.GetFileInfoSync(id); // database opp
HttpResponse response = HttpContext.Current.Response;
response.Clear();
response.ClearContent();
response.Buffer = true;
response.AddHeader("Content-Disposition", "attachment; filename=\"" + fileInfo.Node.Name + fileInfo.Ext + "\"");
response.AddHeader("Content-Length", fileInfo.SizeInBytes.ToString());
response.ContentType = "application/octet-stream";
await logic.GetDownloadStreamSync(id, response.OutputStream);
//database opp + file I/O
response.StatusCode = (int)HttpStatusCode.OK;
//HttpContext.Current.ApplicationInstance.CompleteRequest();
response.End();
}
With the async implementation of GetDownloadStream as follows: (streamToCopyTo is the OutputStream from the response.OutputStream)
public async Task GetDownloadStream(string fileIdentifier, Stream streamToCopyTo)
{
using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.None, BufferSize, true))
{
await fs.CopyToAsync(streamToCopyTo);
}
}
We are trying to embrace the async/await pattern from front to back, so hopefully someone is aware of why this would be failing? I have also tried not calling Response.End(), Response.Flush(), and HttpContext.Current.ApplicationInstance.CompleteRequest(). Also, in response to the questions/comments below, I have placed a breakpoint on the response.End() with the result of it not being hit to GetDownloadStream method has completed. Perhaps the OutputStream is not async? Any ideas are welcome! Thanks
************************** Final Solution ***************************
Big thanks to everyone who commented, and especially to @Noseratio for his suggestion on the FileOptions.DeleteOnClose.
[HttpGet]
public async Task<HttpResponseMessage> Get(long id)
{
HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK);
Node node = await logic.GetFileInfoForNodeAsync(id);
result.Content = new StreamContent(await logic.GetDownloadStreamAsync(id));
result.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
{
FileName = node.Name + node.FileInfo.Extension
};
result.Content.Headers.ContentLength = node.FileInfo.SizeInBytes;
return result
}
With the GetDownloadStreamAsync looking like this:
FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.None, BufferSize, FileOptions.DeleteOnClose | FileOptions.Asynchronous);
I left out that I was also decrypting the file stream on the fly, and this does work, so for those interested...
FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.None, BufferSize, FileOptions.DeleteOnClose | FileOptions.Asynchronous);
RijndaelManaged rm = new RijndaelManaged();
return new CryptoStream(fs, GetDecryptor(rm, password), CryptoStreamMode.Read);
It'd take to have a complete repro case to answer your exact question, but I don't think you need async/await
here at all. I also think you should avoid using HttpContext.Current.Response
directly where possible, especially in asynchronous WebAPI controller methods.
In this particular case, you could use HttpResponseMessage
:
[HttpGet]
public HttpResponseMessage Get(int id)
{
HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK);
FileInfo fileInfo = logic.GetFileInfoSync(id);
FileStream fs = new FileStream(
filePath, FileMode.Open, FileAccess.Read, FileShare.None, BufferSize, false);
result.Content = new StreamContent(fs);
result.Content.Headers.ContentType =
new MediaTypeHeaderValue("application/octet-stream");
result.Content.Headers.ContentDisposition =
new ContentDispositionHeaderValue("attachment")
{
FileName = fileInfo.Node.Name + fileInfo.Ext
};
result.Content.Headers.ContentLength = fileInfo.SizeInBytes;
return result;
}
There is no explicit asynchrony here, so the method is not async
. If however you still need to introduce some await
, the method would like like this:
[HttpGet]
public async Task<HttpResponseMessage> Get(int id)
{
HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK);
// ...
await fs.CopyToAsync(streamToCopyTo)
// ...
return result;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With