I have a need to bulk-export content over WebAPI using the Odata protocol. We are trying to stream the results directly out of the database using PushStreamContent. When I run the service in my local IIS instance it works great, but when I push this to the server it will stream the data and the pause and "hang" on the last 2 kilobytes.
I verified that by keeping track of the file size. On a local run I will get a file that is 9094KB, and when I deploy the same code to the server I get 9092KB and then the connection just stays open and transfer stops. If I kill the client and look at the file I will see the json that was stream was cut off in mid write. Additionally, I can look at the open connections in IIS and see that connection is still active.
Anyway, so why would the PushStreamContent just appear to stop sending data and not close the stream? If an error was happening the stream and connection would close.
public HttpResponseMessage GetBulkExport(ODataQueryOptions<vwBulkExport> options)
{
var reportData = options.ApplyTo(dbContext.vwBulkExport, new ODataQuerySettings() { EnsureStableOrdering = false });
return new ResponseStreamer(Request).StreamAsync(reportData);
}
public class ResponseStreamer
{
private HttpRequestMessage request;
public ResponseStreamer(HttpRequestMessage request)
{
this.request = request;
}
public HttpResponseMessage StreamAsync(IQueryable data)
{
HttpResponseMessage response = request.CreateResponse();
response.Content = new PushStreamContent(
async (outputStream, httpContent, transportContext) =>
{
try
{
int counter = 0;
foreach (var item in data)
{
counter++;
string json = JsonConvert.SerializeObject(item);
var buffer = Encoding.UTF8.GetBytes(json);
await outputStream.WriteAsync(buffer, 0, buffer.Length);
if (counter == 10)
{
counter = 0;
await outputStream.FlushAsync();
}
}
}
finally
{
await outputStream.FlushAsync();
outputStream.Close();
outputStream.Dispose();
}
});
return response;
}
}
Here is my client code
using (var writer = File.OpenWrite("C:\\temp\\" + Guid.NewGuid().ToString()))
{
var client = new RestClient("http://localhost");
var url = "/odata/BulkExport";
var request = new RestRequest(url);
request.AddHeader("authorization", string.Format("Bearer {0}", authToken));
request.ResponseWriter = (responseStream) => responseStream.CopyTo(writer);
var response = client.DownloadData(request);
}
I have bee doing extensive testing, and what I think is happening is that the stream is never closed (and thus the last chunk is never sent over) I have come to this conclusion by changing the data iteration above to this:
for (int count = 0; count < 1000; count++) //foreach (var item in data)
{
string json = JsonConvert.SerializeObject(count.ToString()) + Environment.NewLine;
var buffer = Encoding.Default.GetBytes(json);
await outputStream.WriteAsync(buffer, 0, buffer.Length);
}
What I see happening is that it would only return 600 "rows". Again 2 KB seemed to be missing. I then changed the loop to be count <601
and the entire stream was transferred, but the stream never closes. What I think is happening is the internal buffer size if somewhere around 4K (which is what the number 0-600 printed out are) and since the stream isn't closing, the last few bytes are never received. Did that make sense?
Anyway, why would the stream not close? I have it in the finally, and I am not seeing any errors thrown.
I have uncovered more information. The HTTP 1.1 spec says that chunked streams need to end with a zero length chunk. After a bit of digging I found out that it should be happening, but for whatever reason it isn't. In my client I removed the Connection: Keep-Alive
header and replaced it with Connection: Close
I have the same problem, but as soon as I force the connection to close (via shutting down my test app) the last few bytes write to disk and all is good. This is how I know the zero length chunk is not being sent.
So now the question has become. Why is the final zero-length chunk not being sent when I close the stream? From what I have read, calling HttpContext.Current.ApplicationInstance.CompleteRequest();
should force the request to end and write out that chunk. I added that as the last line in my finally block, and using the debugger I know it is running. However, that chunk is still not set.
Remember, all this works on my dev machine hosted in IIS, but not on the web server.
My machine is running Windows 10, I have asp.net 5 installed, and IIS7 with all the defaults.
The web server is Windows Server (I am not sure of the version) Running IIS8 BUT it only has asp.net 4.5 installed. My initial hunch was that this was the problem - different version of the asp.net framework, but I checked and the project is only targeting ASP.NET 4.5. I am still going to try to update the server, but since I am targeting 4.5 I don't think it will do any good.
I think the problem is due to a bug in the .net framework. After updating the server to .net 4.6 it is working.
Here is the patch I applied. https://www.microsoft.com/en-us/download/details.aspx?id=48137