Skip to content
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

util+server: Fix bug around chunked request handling. #6906

Merged

Conversation

philipaconrad
Copy link
Contributor

What are the changes in this PR?

This PR fixes a request handling bug introduced in #6868, which caused OPA to treat all incoming chunked requests as if they had zero-length request bodies.

The fix detects cases where the request body size is unknown in the DecodingLimits handler, and propagates a request context key down to the util.ReadMaybeCompressedBody function, allowing it to correctly select between using the original io.ReadAll style for chunked requests, or the newer preallocated buffers approach (for requests of known size).

This change has a small, but barely visible performance impact for large requests (<5% increase in GC pauses for a 1GB request JSON blob), and minimal, if any, effect on RPS under load.

Fixes: #6904

Notes to assist PR review:

  • A new context key is passed down from the DecodingLimits handler wrapper in server/server.go to the util.ReadMaybeCompressedBody function in util/read_gzip_body.go. This is used to signal cases where the request body is of indeterminate length, and the body reader should enforce the size limit itself.
  • Added back the original io.ReadAll code path in util.ReadMaybeCompressedBody, but it only triggers now on chunked requests, and is guarded both at the top-level in the DecodingLimits handler with a MaxBytesReader, and at the io.ReadAll callsite with a LimitReader. This prevents accidentally reading more of the request body than intended.
  • Gzip handling did not need any changes for this fix, surprisingly! It takes whatever content we pulled from the incoming request/request-stream, and processes it exactly as before.
  • Test cases were extended to throw errors on unexpected warnings, which allows us to catch the issue in Chunked HTTP requests broken in 0.67.0 #6904 that the tests didn't surface back in server+util: Limit max request sizes, prealloc request buffers #6868.

Further notes:

  • Performance impact is minimal: RPS under load seems to be unaffected, and GC overheads increased by <5% for 1GB requests in my local testing.

Copy link
Contributor Author

@philipaconrad philipaconrad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some notes for reviewers:

// For chunked rqeuests (where full size is not known in advance),
// pass server.decoding.max_length down, using the request context.
if r.ContentLength < 0 || (r.ContentLength == 0 && r.Body != nil) {
ctx := util_decoding.AddServerDecodingMaxLen(r.Context(), maxLength)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where the new context key is inserted. Note that we only add it when we have an "indeterminate body size" case, such as what happens during chunked requests.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this only the case for chunked requests or are there some other use cases? If only true for chunked requests, can we just check the header (ie. 'Transfer-Encoding: chunked')?

Also it would be helpful to add a comment which explain the 2 conditions in the if statement.

@@ -1713,8 +1713,10 @@ func generateJSONBenchmarkData(k, v int) map[string]interface{} {
}

return map[string]interface{}{
"keys": keys,
"values": values,
"input": map[string]interface{}{
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a minor refactoring that silences "'input' key missing" warnings from the testcases.

// incremental read of the body. In this case, we can't be too clever, we
// just do the best we can with whatever is streamed over to us.
// Fetch gzip payload size limit from request context.
if maxLength, ok := decoding.GetServerDecodingMaxLen(r.Context()); ok {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where we add back the original io.ReadAll code path. It's only used when we have an indeterminate request body length case, otherwise we fall back to the preallocated buffer logic from #6868.

Copy link
Member

@ashutosh-narkar ashutosh-narkar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fix @philipaconrad!

// For chunked rqeuests (where full size is not known in advance),
// pass server.decoding.max_length down, using the request context.
if r.ContentLength < 0 || (r.ContentLength == 0 && r.Body != nil) {
ctx := util_decoding.AddServerDecodingMaxLen(r.Context(), maxLength)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this only the case for chunked requests or are there some other use cases? If only true for chunked requests, can we just check the header (ie. 'Transfer-Encoding: chunked')?

Also it would be helpful to add a comment which explain the 2 conditions in the if statement.

@philipaconrad
Copy link
Contributor Author

Thanks for the review comments @ashutosh-narkar!

Is this only the case for chunked requests or are there some other use cases?

Searching in the sources of net/http's request.go indicates there are a max of 2x cases where we'd see ContentLength = -1:

  • Chunked transfer-encoding
  • An upgrade-to-HTTP2 edge case

If only true for chunked requests, can we just check the header (ie. 'Transfer-Encoding: chunked')?

net/http actually adds and removes the chunked encoding header transparently in most request/response cases, so we will never see that header explicitly set under normal conditions (I ran some local tests to verify this). There is a request field called TransferEncoding that seems to be populated when chunked encoding is present, but ContentLength will always be -1 in those cases, so I've kept just the ContentLength check.

Also it would be helpful to add a comment which explain the 2 conditions in the if statement.

I've embellished the original comment to explain the why behind the "magic" check, and have simplified down to just 1x check, because the other conditional check applies only to client requests, where Golang is sending a request, not receiving it.

There's a bit of history documented in the sources around why that condition existed, and it dates back to backwards compatibility stuff in Go 1.8.

ashutosh-narkar
ashutosh-narkar previously approved these changes Aug 1, 2024
This commit fixes a request handling bug introduced in open-policy-agent#6868, which
caused OPA to treat all incoming chunked requests as if they had
zero-length request bodies.

The fix detects cases where the request body size is unknown in the
DecodingLimits handler, and propagates a request context key down to
the `util.ReadMaybeCompressedBody` function, allowing it to correctly
select between using the original `io.ReadAll` style for chunked
requests, or the newer preallocated buffers approach (for requests of
known size).

This change has a small, but barely visible performance impact for large
requests (<5% increase in GC pauses for a 1GB request JSON blob), and
minimal, if any, effect on RPS under load.

Fixes: open-policy-agent#6904

Signed-off-by: Philip Conrad <philip@chariot-chaser.net>
Copy link

netlify bot commented Aug 1, 2024

Deploy Preview for openpolicyagent ready!

Name Link
🔨 Latest commit 16dee19
🔍 Latest deploy log https://app.netlify.com/sites/openpolicyagent/deploys/66abdd0073551f0008d2e215
😎 Deploy Preview https://deploy-preview-6906--openpolicyagent.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

@philipaconrad philipaconrad merged commit ee9ab0b into open-policy-agent:main Aug 1, 2024
28 checks passed
ashutosh-narkar pushed a commit to ashutosh-narkar/opa that referenced this pull request Aug 1, 2024
…ent#6906)

This commit fixes a request handling bug introduced in open-policy-agent#6868, which
caused OPA to treat all incoming chunked requests as if they had
zero-length request bodies.

The fix detects cases where the request body size is unknown in the
DecodingLimits handler, and propagates a request context key down to
the `util.ReadMaybeCompressedBody` function, allowing it to correctly
select between using the original `io.ReadAll` style for chunked
requests, or the newer preallocated buffers approach (for requests of
known size).

This change has a small, but barely visible performance impact for large
requests (<5% increase in GC pauses for a 1GB request JSON blob), and
minimal, if any, effect on RPS under load.

Fixes: open-policy-agent#6904

Signed-off-by: Philip Conrad <philip@chariot-chaser.net>
(cherry picked from commit ee9ab0b)
ashutosh-narkar pushed a commit that referenced this pull request Aug 5, 2024
This commit fixes a request handling bug introduced in #6868, which
caused OPA to treat all incoming chunked requests as if they had
zero-length request bodies.

The fix detects cases where the request body size is unknown in the
DecodingLimits handler, and propagates a request context key down to
the `util.ReadMaybeCompressedBody` function, allowing it to correctly
select between using the original `io.ReadAll` style for chunked
requests, or the newer preallocated buffers approach (for requests of
known size).

This change has a small, but barely visible performance impact for large
requests (<5% increase in GC pauses for a 1GB request JSON blob), and
minimal, if any, effect on RPS under load.

Fixes: #6904

Signed-off-by: Philip Conrad <philip@chariot-chaser.net>
(cherry picked from commit ee9ab0b)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Chunked HTTP requests broken in 0.67.0
2 participants