Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# Async ApiController Closing OutputStream Prematurely

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);
like image 431
TChadwick Avatar asked Feb 10 '15 18:02

TChadwick


1 Answers

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;
}
like image 154
noseratio Avatar answered Oct 15 '22 02:10

noseratio