-
Notifications
You must be signed in to change notification settings - Fork 6.3k
8263031: HttpClient throws Exception if it receives a Push Promise that is too large #7696
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
Conversation
|
👋 Welcome back ccleary! A progress list of the required criteria for merging this PR into |
Webrevs
|
|
I see that a couple of imports got changed by my IDE, will address that shortly |
Now resolved |
| Map<String, List<String>> map = new HashMap<>(); | ||
| map.put("x-promise", List.of(mainPromiseBody)); | ||
| HttpHeaders headers = HttpHeaders.of(map, ACCEPT_ALL); | ||
| exchange.serverPush(uri, headers, is); |
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.
Could create all of the headers here instead of in Http2LPPTestExchangeImpl, might improve readability of test.
| import javax.net.ssl.SSLEngine; | ||
| import javax.net.ssl.SSLException; | ||
| import java.io.EOFException; | ||
| import java.io.IOException; | ||
| import java.io.UncheckedIOException; | ||
| import java.net.InetSocketAddress; | ||
| import java.net.URI; | ||
| import java.net.http.HttpClient; | ||
| import java.net.http.HttpHeaders; | ||
| import java.nio.ByteBuffer; | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.util.ArrayList; | ||
| import java.util.Iterator; | ||
| import java.util.List; | ||
| import java.util.Locale; | ||
| import java.util.Map; | ||
| import java.util.Objects; | ||
| import java.util.Set; | ||
| import java.util.concurrent.CompletableFuture; | ||
| import java.util.concurrent.ConcurrentHashMap; | ||
| import java.util.concurrent.ConcurrentLinkedQueue; | ||
| import java.util.concurrent.ConcurrentMap; | ||
| import java.util.concurrent.Flow; | ||
| import java.util.function.Function; | ||
| import java.util.function.Supplier; | ||
|
|
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.
Could we keep the original alphabetical ordering and have the java.* imports come before the jdk.* imports?
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.
Done, this was just an IDE hiccup
src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java
Outdated
Show resolved
Hide resolved
src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java
Outdated
Show resolved
Hide resolved
| if (frame instanceof ContinuationFrame cf) { | ||
| handlePushContinuation(stream, cf); | ||
| } else { | ||
| // TODO: Maybe say what kind of frame was received instead |
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 should either do it or remove the TODO. @Michael-Mc-Mahon what would you suggest here?
| // stream is a Continuation frame | ||
| if (pcs != null) { | ||
| if (frame instanceof ContinuationFrame cf) { | ||
| handlePushContinuation(stream, cf); |
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.
IOException should probably be caught and transformed in protocol error here to?
In case of protocol error when handling push promise (or their continuation) - should pcs be reset to null?
Maybe it doesn't matter since we're closing the connection anyway.
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.
In my opinion it would probably be safest to set pcs to null here yes.
| * @run testng/othervm PushPromiseContinuation | ||
| */ |
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 would be good to add an @summary tag to explain the purpose of this test
| System.err.println("Server: Push sent"); | ||
| } | ||
| } | ||
| } No newline at end of file |
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.
missing newline at end of file?
| OutgoingPushPromise pp = new OutgoingPushPromise(streamid, uri, pushPromiseHeaders, content); | ||
| // Indicates to the client that a continuation should be expected | ||
| pp.setFlag(0x0); | ||
|
|
||
| // Create the continuation frame | ||
| List<ByteBuffer> encodedHeaders = conn.encodeHeaders(continuationHeaders); | ||
| ContinuationFrame cf = new ContinuationFrame(streamid, HeaderFrame.END_HEADERS, encodedHeaders); | ||
|
|
||
| try { |
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 would be good to have a test-case that creates 2 continuation frames too.
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.
Good idea yes, to check that the repeat continuation still behaves as expected. Should hopefully be straight forward to create another test case.
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.
On this issue, there is a case where a faulty server might send an indefinite number of Continuations (maybe the server never sets an END_HEADERS flag). Should a safe guard for the Push Promise with Continuation/s case be put in place to prevent the faulty scenario?
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 would be impossible to detect, but if the server is faulty and "forget" to set the END_HEADERS flag, then we will detect that because the next frame we receive won't be the ContinuationFrame we expect.
| } | ||
|
|
||
| private record PushContinuationState(HeaderDecoder pushContDecoder, PushPromiseFrame pushContFrame) {} | ||
| private volatile PushContinuationState pcs; |
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'd prefer to have a longer name than pcs - pushContinuationState would make it less mysterious at the place where it's used. Also it could be good to move these declarations (linew 854 and 855) higher in the file, where other instance variables are declared.
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.
Changed to pushContinuationState, its a long name but definitely clearer
| // TODO: Maybe say what kind of frame was received instead | ||
| protocolError(ErrorFrame.PROTOCOL_ERROR, "Expected Continuation frame but received another type"); | ||
| pushContinuationState = null; | ||
| protocolError(ErrorFrame.PROTOCOL_ERROR, "Expected a Continuation frame but received " + frame); |
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.
In all other places in this method we have return; just after a call to protocolError, except in the two places where your changes added one. For consistency you should probably add this return; statement, even if it's not strictly needed. It would avoid having to have to analyze the whole structure of the nested if - then - else to figure out that it's actually not needed.
| private <T> void handlePushContinuation(Stream<T> parent, ContinuationFrame cf) | ||
| throws IOException { | ||
| decodeHeaders(cf, pcs.pushContDecoder); | ||
| decodeHeaders(cf, pushContinuationState.pushContDecoder); |
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 suggest declaring a local variable here to avoid reading pushContinuationState more than once.
Something like:
var pcs = pushContinuationState;
then use pcs wherever needed in that method.
|
|
||
| static HttpHeaders testHeaders; | ||
| static HttpHeadersBuilder testHeadersBuilder; | ||
| static int continuationCount; |
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.
Since these three static variables are set by one thread and read by another - they should all be volatile.
dfuch
left a comment
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.
Thanks for making these changes! Still a few comments...
|
|
||
| package jdk.internal.net.http; | ||
|
|
||
| import jdk.internal.net.http.HttpConnection.HttpPublisher; |
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.
Could this line be pushed down to where other jdk.* imports are made?
| HttpRequest hreq = HttpRequest.newBuilder(uri).version(HttpClient.Version.HTTP_2).GET().build(); | ||
| CompletableFuture<HttpResponse<String>> cf = | ||
| client.sendAsync(hreq, HttpResponse.BodyHandlers.ofString(UTF_8), pph); | ||
| cf.join(); |
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'd suggest checking that we received the expected response code + body too
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.
Further changes made to check response codes and bodies of both the response and push promise.
| handlePushContinuation(stream, cf); | ||
| } catch (UncheckedIOException e) { | ||
| debug.log("Error handling Push Promise with Continuation: " + e.getMessage(), e); | ||
| protocolError(ResetFrame.PROTOCOL_ERROR, e.getMessage()); |
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 believe it would more clear to use ErrorFrame.PROTOCOL_ERROR or GoAwayFrame.PROTOCOL_ERRROR here and in the other calls to protocolError below, since we're not resetting the stream here but closing the whole connection with a GoAwayFrame.
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.
Oh yes, good point. I think ErrorFrame.PROTOCOL_ERROR would be the most appropriate here. I'll amend the change accordingly.
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.
However, I'll look into the specification further for the other cases and see if they need be changed as well. Though closing the whole connection with GoAwayFrame seems correct
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 changed three more occurences to ErrorFrame.PROTOCOL_ERROR, its easier to understand and lines up more clearly with specification
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.
Make sure to run tier1, tier2 before integrating. Drop me a note when you are ready I will sponsor the change.
|
@c-cleary This change now passes all automated pre-integration checks. ℹ️ This project also has non-automated pre-integration requirements. Please see the file CONTRIBUTING.md for details. After integration, the commit message for the final commit will be: You can use pull request commands such as /summary, /contributor and /issue to adjust it as needed. At the time when this comment was updated there had been 425 new commits pushed to the
As there are no conflicts, your changes will automatically be rebased on top of these commits when integrating. If you prefer to avoid this automatic rebasing, please check the documentation for the /integrate command for further details. As you do not have Committer status in this project an existing Committer must agree to sponsor your change. Possible candidates are the reviewers of this PR (@dfuch) but any other Committer may sponsor as well. ➡️ To flag this PR as ready for integration with the above commit message, type |
|
/integrate |
|
/sponsor |
|
Going to push as commit 4d2cd26.
Your commit was automatically rebased without conflicts. |
Problem
When a Continuation Frame is received by the httpclient using HTTP/2 after a Push Promise frame (can happen if the amount of headers to be sent in a single Push Promise frame exceeds the maximum frame size, so a Continuation frame is required), the following exception occurs:
This exception occurs because there is no existing flow in
jdk/internal/net/http/Http2Connection.javawhich accounts for the case where a PushPromiseFrame is received with the END_HEADERS flag set to 0x0. When this occurs, the only acceptable frame/s (as multiple continuations are also acceptable) that can be received by the client on the same stream is a continuation frame.Fix
To ensure correct behavior, the following changes were made to
jdk/internal/net/http/Http2Connection.java.handlePushPromise()was modified so that if the END_HEADERS flag is unset (flags equal to 0x0), then a record used to track the state of the Push Promise containing a sharedHeaderDecoderand the receivedPushPromiseFrameis initialised.ContinuationFrameis received inprocessFrame(), the methodhandlePushContinuation()is called instead of the default flow resulting instream.incoming(frame)being called (the source of the incorrect behaviour originally).handlePushContinuation(), the shared decoder is used to decode the receivedContinuationFrameheaders and if theEND_HEADERSflag is set (flags equal to 0x4), theHttpHeadersobject for the Push Promise as a whole is constructed which serves to combine the headers from both thePushPromiseFrameand theContinuationFrame.A regression test was included which verifies that the exception is not thrown and that the headers arrive correctly.
Progress
Issue
Reviewers
Reviewing
Using
gitCheckout this PR locally:
$ git fetch https://git.openjdk.java.net/jdk pull/7696/head:pull/7696$ git checkout pull/7696Update a local copy of the PR:
$ git checkout pull/7696$ git pull https://git.openjdk.java.net/jdk pull/7696/headUsing Skara CLI tools
Checkout this PR locally:
$ git pr checkout 7696View PR using the GUI difftool:
$ git pr show -t 7696Using diff file
Download this PR as a diff file:
https://git.openjdk.java.net/jdk/pull/7696.diff