HttpRequest.BodyReader.TryRead returns false even though request has a body
Xor-el opened this issue · comments
Is there an existing issue for this?
- I have searched the existing issues
Describe the bug
HttpRequest.BodyReader.TryRead
returns false
(and ReadResult
isn't populated) even though the request has a body and the readResult
isn't completed or cancelled.
When using HttpRequest.BodyReader.ReadAsync
, the ReadResult
response is populated successfully.
Here is a Demo that reproduces the issue.
Expected Behavior
HttpRequest.BodyReader.TryRead
should not return false
when a request body exists and the readResult
isn't cancelled or completed.
Steps To Reproduce
-
Clone this Repo, build and run.
-
Fire a
POST
request tohttps://localhost:7244/bodyreaderbugdemo/working
with the request body below
{
"name": "Chucky",
"description": "Child's Play is an American slasher media franchise created by Don Mancini."
}
Notice that the response is a 200 Ok
- Now, fire another
POST
request tohttps://localhost:7244/bodyreaderbugdemo/buggy
with the same request body as above.
Notice that we get a 400 Bad Request
Key Notes
-
Both endpoints employ filters that shortcircuits if the read request body is
null
. -
The first endpoint uses a filter
ExtractRequestBodyWorking
which usesHttpRequest.BodyReader.ReadAsync
to read request body. This works as expected. -
However, the second endpoint uses another filter
ExtractRequestBodyBuggy
which usesHttpRequest.BodyReader.TryRead
to read request body. in this case,HttpRequest.BodyReader.TryRead
returnsfalse
even though a request body exists.
Exceptions (if any)
No response
.NET Version
8.0.200
Anything else?
No response
This is a misunderstanding. TryRead returns false if data isn't immediately available. TryRead or ReadAsync may still return data in the future. The request may have a large body, but it isn't available yet.
I would also suggest trying this without request body buffering, that construct can skew your results since it involves pipe<->stream adapters, you don't have direct access to the server buffers.
https://github.com/dotnet/runtime/blob/52638488c45ef62d1e9273902f57c010c6310f1d/src/libraries/System.IO.Pipelines/src/System/IO/Pipelines/StreamPipeReader.cs#L465-L468
I would also suggest trying this without request body buffering, that construct can skew your results since it involves pipe<->stream adapters, you don't have direct access to the server buffers. https://github.com/dotnet/runtime/blob/52638488c45ef62d1e9273902f57c010c6310f1d/src/libraries/System.IO.Pipelines/src/System/IO/Pipelines/StreamPipeReader.cs#L465-L468
Thanks for replying @Tratcher .
I read some where some months back that Asp.Net Core does not buffer request body by default so if we need to read the body multiple times, we have to enable request.EnableBuffering
.
To clear all doubts, I tried your suggestion, I removed request.EnableBuffering
and now when trying to read using either ReadAsync
or TryRead
, they fail with an exception.
I then removed httpRequest.Body.Seek(0, SeekOrigin.Begin);
and bodyReader.AdvanceTo(readResult.Buffer.Start, readResult.Buffer.Start);
and everything seems to work Ok.
does this mean that I do not have to reset the bodyReader
in the finally block after reading?
You can put that all back, it was just removed to simplify the scenario. The real fix is to replace TryRead with ReadAsync.
You can put that all back, it was just removed to simplify the scenario. The real fix is to replace TryRead with ReadAsync.
Ok. but say I need to use TryRead
, continue
instead of break
would be the proper approach right?
something like the code below.
public static T? ReadBody<T>(this HttpRequest httpRequest)
{
var bodyReader = httpRequest.BodyReader;
ReadResult readResult = default;
do
{
if (!bodyReader.TryRead(out readResult))
continue; // continue instead of breaking here ??
bodyReader.AdvanceTo(readResult.Buffer.Start, readResult.Buffer.End);
} while (!readResult.IsCanceled && !readResult.IsCompleted);
var buffer = readResult.Buffer;
if (buffer.IsEmpty)
return default;
var reader = new Utf8JsonReader(buffer);
return JsonSerializer.Deserialize<T>(ref reader, JsonExtensions.JsonOptions);
}
If TryRead returns false then you need to call ReadAsync next, not stop reading entirely.