Skip to content

Commit 1000028

Browse files
committed
8292044: HttpClient doesn't handle 102 or 103 properly
Reviewed-by: phh Backport-of: 800e68d6906734242119e4ea033422f037a79857
1 parent 2ec8717 commit 1000028

File tree

7 files changed

+618
-9
lines changed

7 files changed

+618
-9
lines changed

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

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.io.IOException;
2929
import java.lang.System.Logger.Level;
3030
import java.net.InetSocketAddress;
31+
import java.net.ProtocolException;
3132
import java.net.ProxySelector;
3233
import java.net.URI;
3334
import java.net.URISyntaxException;
@@ -447,10 +448,62 @@ private CompletableFuture<Response> sendRequestBody(ExchangeImpl<T> ex) {
447448
CompletableFuture<Response> cf = ex.sendBodyAsync()
448449
.thenCompose(exIm -> exIm.getResponseAsync(parentExecutor));
449450
cf = wrapForUpgrade(cf);
451+
// after 101 is handled we check for other 1xx responses
452+
cf = cf.thenCompose(this::ignore1xxResponse);
450453
cf = wrapForLog(cf);
451454
return cf;
452455
}
453456

457+
/**
458+
* Checks whether the passed Response has a status code between 102 and 199 (both inclusive).
459+
* If so, then that {@code Response} is considered intermediate informational response and is
460+
* ignored by the client. This method then creates a new {@link CompletableFuture} which
461+
* completes when a subsequent response is sent by the server. Such newly constructed
462+
* {@link CompletableFuture} will not complete till a "final" response (one which doesn't have
463+
* a response code between 102 and 199 inclusive) is sent by the server. The returned
464+
* {@link CompletableFuture} is thus capable of handling multiple subsequent intermediate
465+
* informational responses from the server.
466+
* <p>
467+
* If the passed Response doesn't have a status code between 102 and 199 (both inclusive) then
468+
* this method immediately returns back a completed {@link CompletableFuture} with the passed
469+
* {@code Response}.
470+
* </p>
471+
*
472+
* @param rsp The response
473+
* @return A {@code CompletableFuture} with the final response from the server
474+
*/
475+
private CompletableFuture<Response> ignore1xxResponse(final Response rsp) {
476+
final int statusCode = rsp.statusCode();
477+
// we ignore any response code which is 1xx.
478+
// For 100 (with the request configured to expect-continue) and 101, we handle it
479+
// specifically as defined in the RFC-9110, outside of this method.
480+
// As noted in RFC-9110, section 15.2.1, if response code is 100 and if the request wasn't
481+
// configured with expectContinue, then we ignore the 100 response and wait for the final
482+
// response (just like any other 1xx response).
483+
// Any other response code between 102 and 199 (both inclusive) aren't specified in the
484+
// "HTTP semantics" RFC-9110. The spec states that these 1xx response codes are informational
485+
// and interim and the client can choose to ignore them and continue to wait for the
486+
// final response (headers)
487+
if ((statusCode >= 102 && statusCode <= 199)
488+
|| (statusCode == 100 && !request.expectContinue)) {
489+
Log.logTrace("Ignoring (1xx informational) response code {0}", rsp.statusCode());
490+
if (debug.on()) {
491+
debug.log("Ignoring (1xx informational) response code "
492+
+ rsp.statusCode());
493+
}
494+
assert exchImpl != null : "Illegal state - current exchange isn't set";
495+
// ignore this Response and wait again for the subsequent response headers
496+
final CompletableFuture<Response> cf = exchImpl.getResponseAsync(parentExecutor);
497+
// we recompose the CF again into the ignore1xxResponse check/function because
498+
// the 1xx response is allowed to be sent multiple times for a request, before
499+
// a final response arrives
500+
return cf.thenCompose(this::ignore1xxResponse);
501+
} else {
502+
// return the already completed future
503+
return MinimalFuture.completedFuture(rsp);
504+
}
505+
}
506+
454507
CompletableFuture<Response> responseAsyncImpl0(HttpConnection connection) {
455508
Function<ExchangeImpl<T>, CompletableFuture<Response>> after407Check;
456509
bodyIgnored = null;
@@ -481,7 +534,30 @@ private CompletableFuture<Response> wrapForUpgrade(CompletableFuture<Response> c
481534
if (upgrading) {
482535
return cf.thenCompose(r -> checkForUpgradeAsync(r, exchImpl));
483536
}
484-
return cf;
537+
// websocket requests use "Connection: Upgrade" and "Upgrade: websocket" headers.
538+
// however, the "upgrading" flag we maintain in this class only tracks a h2 upgrade
539+
// that we internally triggered. So it will be false in the case of websocket upgrade, hence
540+
// this additional check. If it's a websocket request we allow 101 responses and we don't
541+
// require any additional checks when a response arrives.
542+
if (request.isWebSocket()) {
543+
return cf;
544+
}
545+
// not expecting an upgrade, but if the server sends a 101 response then we fail the
546+
// request and also let the ExchangeImpl deal with it as a protocol error
547+
return cf.thenCompose(r -> {
548+
if (r.statusCode == 101) {
549+
final ProtocolException protoEx = new ProtocolException("Unexpected 101 " +
550+
"response, when not upgrading");
551+
assert exchImpl != null : "Illegal state - current exchange isn't set";
552+
try {
553+
exchImpl.onProtocolError(protoEx);
554+
} catch (Throwable ignore){
555+
// ignored
556+
}
557+
return MinimalFuture.failedFuture(protoEx);
558+
}
559+
return MinimalFuture.completedFuture(r);
560+
});
485561
}
486562

487563
private CompletableFuture<Response> wrapForLog(CompletableFuture<Response> cf) {

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,16 @@ abstract CompletableFuture<T> readBodyAsync(HttpResponse.BodyHandler<T> handler,
199199
*/
200200
abstract void cancel(IOException cause);
201201

202+
/**
203+
* Invoked whenever there is a (HTTP) protocol error when dealing with the response
204+
* from the server. The implementations of {@code ExchangeImpl} are then expected to
205+
* take necessary action that is expected by the corresponding specifications whenever
206+
* a protocol error happens. For example, in HTTP/1.1, such protocol error would result
207+
* in the connection being closed.
208+
* @param cause The cause of the protocol violation
209+
*/
210+
abstract void onProtocolError(IOException cause);
211+
202212
/**
203213
* Called when the exchange is released, so that cleanup actions may be
204214
* performed - such as deregistering callbacks.

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,15 @@ void cancel(IOException cause) {
427427
cancelImpl(cause);
428428
}
429429

430+
@Override
431+
void onProtocolError(final IOException cause) {
432+
if (debug.on()) {
433+
debug.log("cancelling exchange due to protocol error: %s", cause.getMessage());
434+
}
435+
Log.logError("cancelling exchange due to protocol error: {0}\n", cause);
436+
cancelImpl(cause);
437+
}
438+
430439
private void cancelImpl(Throwable cause) {
431440
LinkedList<CompletableFuture<?>> toComplete = null;
432441
int count = 0;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ private Http2Connection(HttpConnection connection,
344344
sendConnectionPreface();
345345
if (!opened) {
346346
debug.log("ensure reset frame is sent to cancel initial stream");
347-
initialStream.sendCancelStreamFrame();
347+
initialStream.sendResetStreamFrame(ResetFrame.CANCEL);
348348
}
349349

350350
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import java.net.Authenticator;
3737
import java.net.ConnectException;
3838
import java.net.CookieHandler;
39+
import java.net.ProtocolException;
3940
import java.net.ProxySelector;
4041
import java.net.http.HttpConnectTimeoutException;
4142
import java.net.http.HttpTimeoutException;
@@ -582,6 +583,8 @@ private void debugCompleted(String tag, long startNanos, HttpRequest req) {
582583
// any other SSLException is wrapped in a plain
583584
// SSLException
584585
throw new SSLException(msg, throwable);
586+
} else if (throwable instanceof ProtocolException) {
587+
throw new ProtocolException(msg);
585588
} else if (throwable instanceof IOException) {
586589
throw new IOException(msg, throwable);
587590
} else {

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

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ CompletableFuture<ExchangeImpl<T>> sendBodyAsync() {
414414
private boolean checkRequestCancelled() {
415415
if (exchange.multi.requestCancelled()) {
416416
if (errorRef.get() == null) cancel();
417-
else sendCancelStreamFrame();
417+
else sendResetStreamFrame(ResetFrame.CANCEL);
418418
return true;
419419
}
420420
return false;
@@ -1203,6 +1203,16 @@ void cancel(IOException cause) {
12031203
cancelImpl(cause);
12041204
}
12051205

1206+
@Override
1207+
void onProtocolError(final IOException cause) {
1208+
if (debug.on()) {
1209+
debug.log("cancelling exchange on stream %d due to protocol error: %s", streamid, cause.getMessage());
1210+
}
1211+
Log.logError("cancelling exchange on stream {0} due to protocol error: {1}\n", streamid, cause);
1212+
// send a RESET frame and close the stream
1213+
cancelImpl(cause, ResetFrame.PROTOCOL_ERROR);
1214+
}
1215+
12061216
void connectionClosing(Throwable cause) {
12071217
Flow.Subscriber<?> subscriber =
12081218
responseSubscriber == null ? pendingResponseSubscriber : responseSubscriber;
@@ -1214,6 +1224,10 @@ void connectionClosing(Throwable cause) {
12141224

12151225
// This method sends a RST_STREAM frame
12161226
void cancelImpl(Throwable e) {
1227+
cancelImpl(e, ResetFrame.CANCEL);
1228+
}
1229+
1230+
private void cancelImpl(final Throwable e, final int resetFrameErrCode) {
12171231
errorRef.compareAndSet(null, e);
12181232
if (debug.on()) {
12191233
if (streamid == 0) debug.log("cancelling stream: %s", (Object)e);
@@ -1245,25 +1259,25 @@ void cancelImpl(Throwable e) {
12451259
try {
12461260
// will send a RST_STREAM frame
12471261
if (streamid != 0 && streamState == 0) {
1248-
e = Utils.getCompletionCause(e);
1249-
if (e instanceof EOFException) {
1262+
final Throwable cause = Utils.getCompletionCause(e);
1263+
if (cause instanceof EOFException) {
12501264
// read EOF: no need to try & send reset
12511265
connection.decrementStreamsCount(streamid);
12521266
connection.closeStream(streamid);
12531267
} else {
12541268
// no use to send CANCEL if already closed.
1255-
sendCancelStreamFrame();
1269+
sendResetStreamFrame(resetFrameErrCode);
12561270
}
12571271
}
12581272
} catch (Throwable ex) {
12591273
Log.logError(ex);
12601274
}
12611275
}
12621276

1263-
void sendCancelStreamFrame() {
1277+
void sendResetStreamFrame(final int resetFrameErrCode) {
12641278
// do not reset a stream until it has a streamid.
1265-
if (streamid > 0 && markStream(ResetFrame.CANCEL) == 0) {
1266-
connection.resetStream(streamid, ResetFrame.CANCEL);
1279+
if (streamid > 0 && markStream(resetFrameErrCode) == 0) {
1280+
connection.resetStream(streamid, resetFrameErrCode);
12671281
}
12681282
close();
12691283
}

0 commit comments

Comments
 (0)