Skip to content

Commit 4addb57

Browse files
committed
8342075: HttpClient: improve HTTP/2 flow control checks
Reviewed-by: rschmelter, abakhtin Backport-of: 32ac3e713ea4370e496717967fff7de9450d2f69
1 parent 7845f08 commit 4addb57

File tree

12 files changed

+1041
-33
lines changed

12 files changed

+1041
-33
lines changed

src/java.net.http/share/classes/jdk/internal/net/http/Http2ClientImpl.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
import jdk.internal.net.http.common.MinimalFuture;
4242
import jdk.internal.net.http.common.Utils;
4343
import jdk.internal.net.http.frame.SettingsFrame;
44+
45+
import static jdk.internal.net.http.frame.SettingsFrame.INITIAL_CONNECTION_WINDOW_SIZE;
4446
import static jdk.internal.net.http.frame.SettingsFrame.INITIAL_WINDOW_SIZE;
4547
import static jdk.internal.net.http.frame.SettingsFrame.ENABLE_PUSH;
4648
import static jdk.internal.net.http.frame.SettingsFrame.HEADER_TABLE_SIZE;
@@ -265,9 +267,13 @@ int getConnectionWindowSize(SettingsFrame clientSettings) {
265267
int defaultValue = Math.min(Integer.MAX_VALUE,
266268
Math.max(streamWindow, K*K*32));
267269

270+
// The min value is the max between the streamWindow and
271+
// the initial connection window size
272+
int minValue = Math.max(INITIAL_CONNECTION_WINDOW_SIZE, streamWindow);
273+
268274
return getParameter(
269275
"jdk.httpclient.connectionWindowSize",
270-
streamWindow, Integer.MAX_VALUE, defaultValue);
276+
minValue, Integer.MAX_VALUE, defaultValue);
271277
}
272278

273279
/**

src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -975,6 +975,34 @@ private String checkMaxOrphanedHeadersExceeded(HeaderFrame hf) {
975975
return null;
976976
}
977977

978+
// This method is called when a DataFrame that was added
979+
// to a Stream::inputQ is later dropped from the queue
980+
// without being consumed.
981+
//
982+
// Before adding a frame to the queue, the Stream calls
983+
// connection.windowUpdater.canBufferUnprocessedBytes(), which
984+
// increases the count of unprocessed bytes in the connection.
985+
// After consuming the frame, it calls connection.windowUpdater::processed,
986+
// which decrements the count of unprocessed bytes, and possibly
987+
// sends a window update to the peer.
988+
//
989+
// This method is called when connection.windowUpdater::processed
990+
// will not be called, which can happen when consuming the frame
991+
// fails, or when an empty DataFrame terminates the stream,
992+
// or when the stream is cancelled while data is still
993+
// sitting in its inputQ. In the later case, it is called for
994+
// each frame that is dropped from the queue.
995+
final void releaseUnconsumed(DataFrame df) {
996+
windowUpdater.released(df.payloadLength());
997+
dropDataFrame(df);
998+
}
999+
1000+
// This method can be called directly when a DataFrame is dropped
1001+
// before/without having been added to any Stream::inputQ.
1002+
// In that case, the number of unprocessed bytes hasn't been incremented
1003+
// by the stream, and does not need to be decremented.
1004+
// Otherwise, if the frame is dropped after having been added to the
1005+
// inputQ, releaseUnconsumed above should be called.
9781006
final void dropDataFrame(DataFrame df) {
9791007
if (isMarked(closedState, SHUTDOWN_REQUESTED)) return;
9801008
if (debug.on()) {
@@ -1331,11 +1359,12 @@ private void sendConnectionPreface() throws IOException {
13311359
// Note that the default initial window size, not to be confused
13321360
// with the initial window size, is defined by RFC 7540 as
13331361
// 64K -1.
1334-
final int len = windowUpdater.initialWindowSize - DEFAULT_INITIAL_WINDOW_SIZE;
1335-
if (len != 0) {
1362+
final int len = windowUpdater.initialWindowSize - INITIAL_CONNECTION_WINDOW_SIZE;
1363+
assert len >= 0;
1364+
if (len > 0) {
13361365
if (Log.channel()) {
13371366
Log.logChannel("Sending initial connection window update frame: {0} ({1} - {2})",
1338-
len, windowUpdater.initialWindowSize, DEFAULT_INITIAL_WINDOW_SIZE);
1367+
len, windowUpdater.initialWindowSize, INITIAL_CONNECTION_WINDOW_SIZE);
13391368
}
13401369
windowUpdater.sendWindowUpdate(len);
13411370
}
@@ -1722,6 +1751,19 @@ public ConnectionWindowUpdateSender(Http2Connection connection,
17221751
int getStreamId() {
17231752
return 0;
17241753
}
1754+
1755+
@Override
1756+
protected boolean windowSizeExceeded(long received) {
1757+
if (connection.isOpen()) {
1758+
try {
1759+
connection.protocolError(ErrorFrame.FLOW_CONTROL_ERROR,
1760+
"connection window exceeded");
1761+
} catch (IOException io) {
1762+
connection.shutdown(io);
1763+
}
1764+
}
1765+
return true;
1766+
}
17251767
}
17261768

17271769
/**

src/java.net.http/share/classes/jdk/internal/net/http/Stream.java

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -157,14 +157,13 @@ class Stream<T> extends ExchangeImpl<T> {
157157

158158
// send lock: prevent sending DataFrames after reset occurred.
159159
private final Object sendLock = new Object();
160-
161160
/**
162161
* A reference to this Stream's connection Send Window controller. The
163162
* stream MUST acquire the appropriate amount of Send Window before
164163
* sending any data. Will be null for PushStreams, as they cannot send data.
165164
*/
166165
private final WindowController windowController;
167-
private final WindowUpdateSender windowUpdater;
166+
private final WindowUpdateSender streamWindowUpdater;
168167

169168
@Override
170169
HttpConnection connection() {
@@ -203,7 +202,8 @@ private void schedule() {
203202
int size = Utils.remaining(dsts, Integer.MAX_VALUE);
204203
if (size == 0 && finished) {
205204
inputQ.remove();
206-
connection.ensureWindowUpdated(df); // must update connection window
205+
// consumed will not be called
206+
connection.releaseUnconsumed(df); // must update connection window
207207
Log.logTrace("responseSubscriber.onComplete");
208208
if (debug.on()) debug.log("incoming: onComplete");
209209
sched.stop();
@@ -219,7 +219,11 @@ private void schedule() {
219219
try {
220220
subscriber.onNext(dsts);
221221
} catch (Throwable t) {
222-
connection.dropDataFrame(df); // must update connection window
222+
// Data frames that have been added to the inputQ
223+
// must be released using releaseUnconsumed() to
224+
// account for the amount of unprocessed bytes
225+
// tracked by the connection.windowUpdater.
226+
connection.releaseUnconsumed(df);
223227
throw t;
224228
}
225229
if (consumed(df)) {
@@ -271,8 +275,12 @@ private void schedule() {
271275
private void drainInputQueue() {
272276
Http2Frame frame;
273277
while ((frame = inputQ.poll()) != null) {
274-
if (frame instanceof DataFrame) {
275-
connection.dropDataFrame((DataFrame)frame);
278+
if (frame instanceof DataFrame df) {
279+
// Data frames that have been added to the inputQ
280+
// must be released using releaseUnconsumed() to
281+
// account for the amount of unprocessed bytes
282+
// tracked by the connection.windowUpdater.
283+
connection.releaseUnconsumed(df);
276284
}
277285
}
278286
}
@@ -298,12 +306,13 @@ private boolean consumed(DataFrame df) {
298306
boolean endStream = df.getFlag(DataFrame.END_STREAM);
299307
if (len == 0) return endStream;
300308

301-
connection.windowUpdater.update(len);
302-
309+
connection.windowUpdater.processed(len);
303310
if (!endStream) {
311+
streamWindowUpdater.processed(len);
312+
} else {
304313
// Don't send window update on a stream which is
305314
// closed or half closed.
306-
windowUpdater.update(len);
315+
streamWindowUpdater.released(len);
307316
}
308317

309318
// true: end of stream; false: more data coming
@@ -373,8 +382,21 @@ public String toString() {
373382
}
374383

375384
private void receiveDataFrame(DataFrame df) {
376-
inputQ.add(df);
377-
sched.runOrSchedule();
385+
try {
386+
int len = df.payloadLength();
387+
if (len > 0) {
388+
// we return from here if the connection is being closed.
389+
if (!connection.windowUpdater.canBufferUnprocessedBytes(len)) return;
390+
// we return from here if the stream is being closed.
391+
if (closed || !streamWindowUpdater.canBufferUnprocessedBytes(len)) {
392+
connection.releaseUnconsumed(df);
393+
return;
394+
}
395+
}
396+
inputQ.add(df);
397+
} finally {
398+
sched.runOrSchedule();
399+
}
378400
}
379401

380402
/** Handles a RESET frame. RESET is always handled inline in the queue. */
@@ -452,7 +474,7 @@ CompletableFuture<ExchangeImpl<T>> sendBodyAsync() {
452474
this.responseHeadersBuilder = new HttpHeadersBuilder();
453475
this.rspHeadersConsumer = new HeadersConsumer();
454476
this.requestPseudoHeaders = createPseudoHeaders(request);
455-
this.windowUpdater = new StreamWindowUpdateSender(connection);
477+
this.streamWindowUpdater = new StreamWindowUpdateSender(connection);
456478
}
457479

458480
private boolean checkRequestCancelled() {
@@ -486,6 +508,8 @@ void incoming(Http2Frame frame) throws IOException {
486508
if (debug.on()) {
487509
debug.log("request cancelled or stream closed: dropping data frame");
488510
}
511+
// Data frames that have not been added to the inputQ
512+
// can be released using dropDataFrame
489513
connection.dropDataFrame(df);
490514
} else {
491515
receiveDataFrame(df);
@@ -1365,12 +1389,18 @@ void cancel(IOException cause) {
13651389

13661390
@Override
13671391
void onProtocolError(final IOException cause) {
1392+
onProtocolError(cause, ResetFrame.PROTOCOL_ERROR);
1393+
}
1394+
1395+
void onProtocolError(final IOException cause, int code) {
13681396
if (debug.on()) {
1369-
debug.log("cancelling exchange on stream %d due to protocol error: %s", streamid, cause.getMessage());
1397+
debug.log("cancelling exchange on stream %d due to protocol error [%s]: %s",
1398+
streamid, ErrorFrame.stringForCode(code),
1399+
cause.getMessage());
13701400
}
13711401
Log.logError("cancelling exchange on stream {0} due to protocol error: {1}\n", streamid, cause);
13721402
// send a RESET frame and close the stream
1373-
cancelImpl(cause, ResetFrame.PROTOCOL_ERROR);
1403+
cancelImpl(cause, code);
13741404
}
13751405

13761406
void connectionClosing(Throwable cause) {
@@ -1666,6 +1696,13 @@ String dbgString() {
16661696
return dbgString = dbg;
16671697
}
16681698
}
1699+
1700+
@Override
1701+
protected boolean windowSizeExceeded(long received) {
1702+
onProtocolError(new ProtocolException("stream %s flow control window exceeded"
1703+
.formatted(streamid)), ResetFrame.FLOW_CONTROL_ERROR);
1704+
return true;
1705+
}
16691706
}
16701707

16711708
/**

0 commit comments

Comments
 (0)