I'm attempting to stream a large JSON file built on the fly to a client (could be 500 MB+). I'm trying to disable response buffering for a variety of reasons, though mostly for memory efficiency.
I've tried writing directly to the HttpContext.Response.BodyWriter but the response seems to be buffered in memory before writing to the output. The return type of this method is Task.
HttpContext.Response.ContentType = "application/json";
HttpContext.Response.ContentLength = null;
await HttpContext.Response.StartAsync(cancellationToken);
var bodyStream = HttpContext.Response.BodyWriter.AsStream(true);
await bodyStream.WriteAsync(Encoding.UTF8.GetBytes("["), cancellationToken);
await foreach (var item in cursor.WithCancellation(cancellationToken)
.ConfigureAwait(false))
{
await bodyStream.WriteAsync(JsonSerializer.SerializeToUtf8Bytes(item, DefaultSettings.JsonSerializerOptions), cancellationToken);
await bodyStream.WriteAsync(Encoding.UTF8.GetBytes(","), cancellationToken);
await bodyStream.FlushAsync(cancellationToken);
await Task.Delay(100,cancellationToken);
}
await bodyStream.WriteAsync(Encoding.UTF8.GetBytes("]"), cancellationToken);
bodyStream.Close();
await HttpContext.Response.CompleteAsync().ConfigureAwait(false);
Note: I realize this code is very hacky, trying to make it work, then clean it up
I'm using the Task.Delay to verify the response is not being buffered when testing locally as I do not have full production data. I have also tried IAsyncEnumerable and yield return, but that fails because the response is so large that Kestrel thinks the enumerable is infinite.
I've tried
- Setting KestrelServerLimits.MaxResponseBufferSize to a small number, even 0;
- Writing with
HttpContext.Response.WriteAsync - Writing with
HttpContext.Response.BodyWriter.AsStream() - Writing with a pipe writer patter and
HttpContext.Response.BodyWriter - Removing all middleware
- Removing calls to
IApplicationBuilder.UseResponseCompression
Update
- Tried disabling response buffering before setting the
ContentType(so before any writes to the response) with no effect
var responseBufferingFeature = context.Features.Get<IHttpResponseBodyFeature>();
responseBufferingFeature?.DisableBuffering();
Updated Sample Code
This reproduces the issue quite simply. The client doesn't receive any data until response.CompleteAsync() is called.
[HttpGet]
[Route("stream")]
public async Task<EmptyResult> FileStream(CancellationToken cancellationToken)
{
var response = DisableResponseBuffering(HttpContext);
HttpContext.Response.Headers.Add("Content-Type", "application/gzip");
HttpContext.Response.Headers.Add("Content-Disposition", $"attachment; filename=\"player-data.csv.gz\"");
await response.StartAsync().ConfigureAwait(false);
var memory = response.Writer.GetMemory(1024*1024*10);
response.Writer.Advance(1024*1024*10);
await response.Writer.FlushAsync(cancellationToken).ConfigureAwait(false);
await Task.Delay(5000).ConfigureAwait(false);
var str2 = Encoding.UTF8.GetBytes("Bar!\r\n");
memory = response.Writer.GetMemory(str2.Length);
str2.CopyTo(memory);
response.Writer.Advance(str2.Length);
await response.CompleteAsync().ConfigureAwait(false);
return new EmptyResult();
}
private IHttpResponseBodyFeature DisableResponseBuffering(HttpContext context)
{
var responseBufferingFeature = context.Features.Get<IHttpResponseBodyFeature>();
responseBufferingFeature?.DisableBuffering();
return responseBufferingFeature;
}