Reputation: 3750
How to properly stream response in ASP.NET Core? There is a controller like this (UPDATED CODE):
[HttpGet("test")]
public async Task GetTest()
{
HttpContext.Response.ContentType = "text/plain";
using (var writer = new StreamWriter(HttpContext.Response.Body))
await writer.WriteLineAsync("Hello World");
}
Firefox/Edge browsers show
Hello World
, while Chrome/Postman report an error:
The localhost page isn’t working
localhost unexpectedly closed the connection.
ERR_INCOMPLETE_CHUNKED_ENCODING
P.S. I am about to stream a lot of content, so I cannot specify Content-Length header in advance.
Upvotes: 56
Views: 109832
Reputation: 3540
@Developer4993 was correct that to have data sent to the client before the entire response has been parsed, it is necessary to Flush
to the response stream. However, their answer is a bit unconventional with both the DELETE
and the Synchronized.StreamWriter
. Additionally, Asp.Net Core 3.x will throw an exception if the I/O is synchronous.
This is tested in Asp.Net Core 3.1:
[HttpGet]
public async Task Get()
{
Response.ContentType = "text/plain";
StreamWriter sw;
await using ((sw = new StreamWriter(Response.Body)).ConfigureAwait(false))
{
foreach (var item in someReader.Read())
{
await sw.WriteLineAsync(item.ToString()).ConfigureAwait(false);
await sw.FlushAsync().ConfigureAwait(false);
}
}
}
Assuming someReader
is iterating either database results or some I/O stream with a large amount of content that you do not want to buffer before sending, this will write a chunk of text to the response stream with each FlushAsync()
.
For my purposes, consuming the results with an HttpClient
was more important than browser compatibility, but if you send enough text, you will see a chromium browser consume the results in a streaming fashion. The browser seems to buffer a certain quantity at first.
Where this becomes more useful is with the latest IAsyncEnumerable
streams, where your source is either time or disk intensive, but can be yielded a bit at at time:
[HttpGet]
public async Task<EmptyResult> Get()
{
Response.ContentType = "text/plain";
StreamWriter sw;
await using ((sw = new StreamWriter(Response.Body)).ConfigureAwait(false))
{
await foreach (var item in GetAsyncEnumerable())
{
await sw.WriteLineAsync(item.ToString()).ConfigureAwait(false);
await sw.FlushAsync().ConfigureAwait(false);
}
}
return new EmptyResult();
}
You can throw an await Task.Delay(1000)
into either foreach
to demonstrate the continuous streaming.
Finally, @StephenCleary 's FileCallbackResult
works the same as these two examples as well. It's just a bit scarier with the FileResultExecutorBase
from deep in the bowels of the Infrastructure
namespace.
[HttpGet]
public IActionResult Get()
{
return new FileCallbackResult(new MediaTypeHeaderValue("text/plain"), async (outputStream, _) =>
{
StreamWriter sw;
await using ((sw = new StreamWriter(outputStream)).ConfigureAwait(false))
{
foreach (var item in someReader.Read())
{
await sw.WriteLineAsync(item.ToString()).ConfigureAwait(false);
await sw.FlushAsync().ConfigureAwait(false);
}
}
outputStream.Close();
});
}
Update: I'd like to point out an excellent sample repo and related post by @swimburger
Repo,
Article
where he uses a tip from David Fowler to stream asynchronously to the Response.BodyWriter
which is a PipeWriter.
While his article is focused on streaming a zip archive, the concept demonstrates a very efficient alternative approach to streaming a response in ASP.NET Core 6,8 and later.
app.MapGet("/test", async ([FromServices]MyWriter sourceWriter, HttpContext context) =>
{
context.Response.ContentType = "text/plain";
await sourceWriter.WriteAsync(context.Response.BodyWriter);
});
where MyWriter
writes and flushes asynchronously to a PipeWriter
or to a Stream
(pass in context.Response.BodyWriter.AsStream()
instead). E.g.
public class MyWriter
{
public async Task WriteAsync(PipeWriter writer)
{
for(int i = 0; i < 1000; i++)
{
await writer.WriteAsync(Encoding.UTF8.GetBytes($"DataItem {i:D4}\n"));
await writer.FlushAsync();
}
}
}
Upvotes: 25
Reputation: 449
This question is a bit older, but I couldn't find a better answer anywhere for what I was trying to do. To send the currently buffered output to the client, you must call Flush()
for each chunk of content you would like to write. Simply do the following:
[HttpDelete]
public void Content()
{
Response.StatusCode = 200;
Response.ContentType = "text/html";
// the easiest way to implement a streaming response, is to simply flush the stream after every write.
// If you are writing to the stream asynchronously, you will want to use a Synchronized StreamWriter.
using (var sw = StreamWriter.Synchronized(new StreamWriter(Response.Body)))
{
foreach (var item in new int[] { 1, 2, 3, 4, })
{
Thread.Sleep(1000);
sw.Write($"<p>Hi there {item}!</p>");
sw.Flush();
}
};
}
you can test with curl using the following command: curl -NX DELETE <CONTROLLER_ROUTE>/content
Upvotes: 2
Reputation: 9502
It is possible to return null
or EmptyResult()
(which are equivalent), even when previously writing to Response.Body
. It may be useful if the method returns ActionResult
to be able to use all the other results aswell (e.g. BadQuery()
) easily.
[HttpGet("test")]
public ActionResult Test()
{
Response.StatusCode = 200;
Response.ContentType = "text/plain";
using (var sw = new StreamWriter(Response.Body))
{
sw.Write("something");
}
return null;
}
Upvotes: 4
Reputation: 147
I was wondering as well how to do this, and have found out that
the original question's code actually works OK on ASP.NET Core 2.1.0-rc1-final
, neither Chrome (and few other browsers) nor JavaScript application do not fail with such endpoint.
Minor things I would like to add are just set StatusCode and close the response Stream to make the response fulfilled:
[HttpGet("test")]
public void Test()
{
Response.StatusCode = 200;
Response.ContentType = "text/plain";
using (Response.Body)
{
using (var sw = new StreamWriter(Response.Body))
{
sw.Write("Hi there!");
}
}
}
Upvotes: 1
Reputation: 457217
To stream a response that should appear to the browser like a downloaded file, you should use FileStreamResult
:
[HttpGet]
public FileStreamResult GetTest()
{
var stream = new MemoryStream(Encoding.ASCII.GetBytes("Hello World"));
return new FileStreamResult(stream, new MediaTypeHeaderValue("text/plain"))
{
FileDownloadName = "test.txt"
};
}
Upvotes: 83
Reputation: 36736
something like this might work:
[HttpGet]
public async Task<IActionResult> GetTest()
{
var contentType = "text/plain";
using (var stream = new MemoryStream(Encoding.ASCII.GetBytes("Hello World")))
return new FileStreamResult(stream, contentType);
}
Upvotes: -2