-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
#1971 #928 Avoid content if original request has no content and avoid Transfer-Encoding: chunked if Content-Length is known #1972
#1971 #928 Avoid content if original request has no content and avoid Transfer-Encoding: chunked if Content-Length is known #1972
Conversation
Thanks for PR recreation following our development process! |
No, you have not and I have a wireshark dump, that proves that. If you set the Request.Content to a HttpContent, the framework will add headers when sending the request. Depending on TryComputeLength it will be Content-Length or Transfer-Encoding.
Please go forward, I tested it, and it worked. But I have no good idea how to test that automatically.
Sure, but I in my opinion it is not up to Ocelot to change the request and add content. If the client request with empty content, Ocelot should send the request with empty content and if the client requests with no content, Ocelot should send the request without content. And if you look in my acceptance tests you will see, that my code does exactly that. |
Mmh, I was just writing that you were right on that part: We should ensure that, "should" meaning it's not the case yet.
Yes, it worked for you. You need to understand that we tested it too and we were very careful with the release since we refactored some important parts of the application. So now, I would like to make sure we are not introducing bugs with your PR. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, you are right. We might need to add Content null for methods like HEAD or TRACE. But, I think you are going too far with a custom Http Content class for empty objects. I have written a lighter version:
if (request.Body == null || (!request.ContentLength.HasValue && StringValues.IsNullOrEmpty(request.Headers.TransferEncoding)))
{
return null;
}
HttpContent content = request.ContentLength is 0
? new ByteArrayContent([])
: new StreamHttpContent(request.HttpContext);
AddContentHeaders(request, content);
return content;
When creating the HttpContent object, we are setting the headers, this include the Content-Length header. So I would prefer using Headers.ContentLength instead of introducing a variable in HttpContent.
@@ -0,0 +1,15 @@ | |||
namespace Ocelot.Request.Mapper; | |||
|
|||
public class EmptyHttpContent : HttpContent |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would recommend using an existing content type instead. eg. ByteArrayContent
HttpContent content; | ||
|
||
// no content if we have no body or if the request has no content according to RFC 2616 section 4.3 | ||
if (request.Body == null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not use this instead, it's imo more compact.
if (request.Body == null || (!request.ContentLength.HasValue && StringValues.IsNullOrEmpty(request.Headers.TransferEncoding)))
{
return null;
}
HttpContent content = request.ContentLength is 0
? new ByteArrayContent([])
: new StreamHttpContent(request.HttpContext);
AddContentHeaders(request, content);
return content;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ggnaegi Gui, is it resolved?
@@ -8,25 +8,24 @@ public class StreamHttpContent : HttpContent | |||
private const int DefaultBufferSize = 65536; | |||
public const long UnknownLength = -1; | |||
private readonly HttpContext _context; | |||
private readonly long _contentLength; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, it's fine like this, but I would prefer using Headers.ContentLength. This value is set when calling AddContentHeaders method and setting the content headers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And...
|
||
protected override bool TryComputeLength(out long length) | ||
{ | ||
length = -1; | ||
return false; | ||
length = _contentLength; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer Headers.ContentLength ?? UnknownLength
so the content length reflects the headers' value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the headers do not contains the Content-Length header, when it will try to Calculate the content length using TryComputeLength. And on chunked content, there is no Content-Length header set, so it will result in a stack overflow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, just use length = Headers.ContentLength ?? UnknownLength
Headers.ContentLength is a long?, so it is per default null, even if the header is not set.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer this approach, so we are relying again on Microsoft implementations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See the implementation of Headers.ContentLength: https://github.com/microsoft/referencesource/blob/51cf7850defa8a17d815b4700b67116e3fa283c2/System/net/System/Net/Http/Headers/HttpContentHeaders.cs#L76
It will not just return null, it tries to compute the content length if the header is not explicitly set, which results in a stack overflow when you use it in TryComputeLength.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would say it is pretty uncommon, that the content has to "ask" the Header how long it is, the content should know that without the header. So even if MS will change that, it will be not in the near future.
No, that's your point of view, at the end, it's a content header, so it's per se part of the content
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ggnaegi: Yeah well, Microsoft is using this approach with Headers.ContentLength in YARP, so that's why I'm a bit surprised.
Alexander's concern: the code Headers.ContentLength
leads to stack overflow. First of all, did someone caught this exception?
My recommendation: Moving Headers.ContentLength
out of the method to the very beginning of the Invoke
. So, can we move headers reading to upper contexts, 1 or even 2 levels up?
Will it help to avoid stack overflow error?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are very sorry, but you've provided the link to the repo with the code 5 years old!!! 🤣
Why we should take it into account? 😉
We need fresh .NET Core source!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Valid link is: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/System/Net/Http/Headers/HttpContentHeaders.cs#L42
But seems the code is similar.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did we finish debates here?
@raman-m After a nightly review, I think we should push these changes in the January 24 Release. I have overseen scenarios that were covered by the ByteArrayContent implementation. |
I used the |
Ok, sorry, was a misunderstanding by me.
Sure, I understand that. Maybe I have an idea for an automatic test, I will try that later. |
No, the ByteArrayContent shouldn't add the Content-Length Header. You need to set the value explicitely. Don't understand me wrong, your Empty Content class is probably better, but it's a custom class, whereas ByteArrayContent is maintained by Microsoft. |
@alexreinert Could you review the code and maybe push the changes later today, thanks! |
I added tests for streaming data. The idea of the tests is just to use that much data (100GB), that it would fail because of missing memory if it is not streamed. |
Ok, I changed it. |
We had the same debates 2 months ago. Our decision and compromise: don't change anything in request body. Ocelot should route the body as-is!
Let us know how does Ocelot changes these rules, or doesn't follow? |
@alexreinert Do you have deployed Ocelot in any Production environments? |
Yes |
Same opinion on my side, it should be same. But with version 23 it is not the same. Neither if you send a request without content, nor when you send content with Content-Length Header, which will be "converted" to chunked encoding.
Without Ocelot it works, with Ocelot 22 it works (but the request is changed and gets a Content-Lenght header), with Ocelot 23 it breaks because of the Transfer-Encoding Header Ocelot (or better say the non-null HttpContent in the HttpWebRequest) appends. |
@alexreinert I will review the code in an hour or two. From your side could you deploy this version on an environment and give us a feedback? Thanks. |
I deployed it in our staging environment and I see no issues. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@alexreinert Maybe reduce the size to 1GB, I'm fine with the rest. We'll probably put the changes on some environments for a few days to validate them. Thanks, and you're welcome to contribute, we've got quite a few open issues. We're looking for someone who would like to propose a PR for SSE support. The new Release will be rolled out next week probably @raman-m yes?
@RaynaldM Maybe you could put the changes from this PR on a staging environment? Thanks! |
Well... It depends on the progress which is ~50% today. Please, keep me updated about the testing in your Production env please ❗ @ggnaegi Can I start final review? I guess PR is ready for final code review... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is hard to review the design here, as for me.
But my suggestions are below! 👇
Question: Do we support Chunked Encoding now? If Yes, we have to update docs in the "Not Supported" chapter.
I see acceptance tests in MapRequestTests
class which seems check exactly the case of Chunked Encoding...
HttpContent content; | ||
|
||
// no content if we have no body or if the request has no content according to RFC 2616 section 4.3 | ||
if (request.Body == null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ggnaegi Gui, is it resolved?
|
||
protected override bool TryComputeLength(out long length) | ||
{ | ||
length = -1; | ||
return false; | ||
length = _contentLength; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ggnaegi: Yeah well, Microsoft is using this approach with Headers.ContentLength in YARP, so that's why I'm a bit surprised.
Alexander's concern: the code Headers.ContentLength
leads to stack overflow. First of all, did someone caught this exception?
My recommendation: Moving Headers.ContentLength
out of the method to the very beginning of the Invoke
. So, can we move headers reading to upper contexts, 1 or even 2 levels up?
Will it help to avoid stack overflow error?
Fixed length content and chunked encoding is supported, depending how the request by the client looks like. And for both ways streaming the content data works. |
Feel free to change the PR, I will quit here. I have not the time to push it any further. |
Got it! Thanks for this PR! We arrange PR delivery ourselves. |
…ncoding: chunked if Content-Length is known
* Added tests for mapping requests
…if 100GB test data
4e1a10b
to
e30ca4d
Compare
@raman-m I'm going to work on this, this afternoon. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code review Round 2
|
||
public class RequestMapper : IRequestMapper | ||
{ | ||
private static readonly HashSet<string> UnsupportedHeaders = new(StringComparer.OrdinalIgnoreCase) { "host", "transfer-encoding" }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ggnaegi Note 1
"transfer-encoding"
{ | ||
HttpContent content; | ||
|
||
// no content if we have no body or if the request has no content according to RFC 2616 section 4.3 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ggnaegi Note 2
A few months ago I proposed to the team to develop the feature which will enforce RFC policies regarding request bodies for GET, HEAD, DELETE verbs... But you and @RaynaldM declined it...
This is the second notification issue from our community!
I believe it is time to develop this feature (introduce empty bodies for special types of HTTP verbs). We can open discussion thread or issue. At least I would like to add TODO...
if (request.Body == null | ||
|| (!request.ContentLength.HasValue && StringValues.IsNullOrEmpty(request.Headers.TransferEncoding))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Old version
- // TODO We should check if we really need to call HttpRequest.Body.Length
- // But we assume that if CanSeek is true, the length is calculated without an important overhead
- if (request.Body is null or { CanSeek: true, Length: <= 0 })
+ // no content if we have no body or if the request has no content according to RFC 2616 section 4.3
+ if (request.Body == null
+ || (!request.ContentLength.HasValue && StringValues.IsNullOrEmpty(request.Headers.TransferEncoding)))
New version
content = request.ContentLength is 0 | ||
? new ByteArrayContent(Array.Empty<byte>()) | ||
: new StreamHttpContent(request.HttpContext); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Old version
- var content = new StreamHttpContent(request.HttpContext);
+ content = request.ContentLength is 0
+ ? new ByteArrayContent(Array.Empty<byte>())
+ : new StreamHttpContent(request.HttpContext);
New version
@@ -8,25 +8,24 @@ public class StreamHttpContent : HttpContent | |||
private const int DefaultBufferSize = 65536; | |||
public const long UnknownLength = -1; | |||
private readonly HttpContext _context; | |||
private readonly long _contentLength; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ready for delivery! ✔️
@ggnaegi But the 2nd follow-up PR is required with your design review, please!
Need your approval!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@raman-m Ok for me, as discussed, I will create a new PR after that.
Follow-up to #1971
With the changes in Ocelot 23.0 the
RequestMapper
sets the PropertyContent
using the newStreamHttpContent
. Because the Content-Length is not computed inStreamHttpContent
the header Transfer-Encoding: chunked will be set on all upstream requests even if there is no content in the downstream request (e.g. on GET requests). This causes issues when the upstream is an IIS working as reverse proxy (using ARR), in that case all GET requests are failing with status code 502.7 inside ARR.Additionally in some cases the Content-Length is required by the upstream server, it will send status code 411 if it is missing, behind Ocelot all request will fail with status code 411, as the Content-Length was not transferred to the upstream server and instead Transfer-Encoding: chunked is sent to the upstream server.
Both issue are addressed with this PR.
Related issues