This question relates specifically to .NET 7+ but it also touches on HTTP basics, so any input is welcome. Also: i tend to do a lot of googling myself before resorting to asking a new question. If my google-fu has left me yet again, i'm more than happy to accept links to external resources that i've overlooked.
I have an MVC endpoint that returns IAsyncEnumerable<int>
. Let's assume that what the endpoint does, is reading rows from a table in a database. The database driver supports querying rows from a cursor using IAsyncEnumerable so i can basically do something like
[HttpGet]
public async IAsyncEnumerable<int> SomeEndpoint() {
await foreach (var row in PerformSelectQuery())
yield return int.Parse(row[0]);
}
So far so good: .NET and System.Text.Json handles it all well:
It sets Transfer-Encoding
to chunked
and starts writing the response as soon as the first row gets returned from the database. I like that, because I don't have to do any buffering so I don't waste memory, and the client can start parsing the result faster (if it supports it).
Let's say: I did a crappy job and the column may contain null values. That would cause an exception to be raised by int.Parse say, in row #24645 out of 90000.
That's a problem:
.NET just kills the connection. Clients like Postman say the request has been aborted. And I can't blame them: There's no way of knowing what went wrong.
I can catch any exceptions (or do other error handling) in my foreach and, if needed, just yield break
, causing the request to fail silently. Again, the client has no idea that the result is incomplete and an error ocurred.
I'm not even sure that's better than having the request fail completely. At least then, the client knows something went wrong.
My current solution is to wrap the int (or whatever i return) in a kind of Result/Option monad. That then gets serialized to the stream instead of the raw value. This way, i can catch an error, yield it as such from my action and then decide to either abort with a yield break, or continue down the table. The specific format of the wrapper is also tricky (polymorphic serialization, yay!) but not really subject of this question.
What other options do I have? I've read a little about the Trailer
header, which seems like it's the right thing to do but
- it apparently has little to no support with existing clients
- I can only imagine how weird usage (from a client) must be
- I have no idea how i would set these trailers in my controller action. The "chunking" is all done internally, so how - in my foreach - would i add error information to the trailer?
- There seems to be no standarized way of reporting errors (gRPC has a little on it, but i'm not implementing a gRPC service).
- And finally, i still can't report a different HTTP status code since in HTTP, that is not a header at all.
EDIT: I have figured out how to enable and write trailers from my function using Response.AppendTrailer, but I need to support HTTP/1.1 and since .NET doesnt support Trailers for HTTP < 2 at all, this won't work anyways :(
Funnily enough, i have noticed that here: Clarification on how IAsyncEnumerable works with ASP.NET Web API two people have started to ask the same question in the comments. Without any reply. Is that such an obscure thing? Is no one streaming IAsyncEnumerable to the client? Or is everyone who does just writing flawless code that never fails? :D
Does anyone know of any best practices for this scenario? Does anyone have experience with a similar problem?