Skip to content

Commit 800e68d

Browse files
committed
8292044: HttpClient doesn't handle 102 or 103 properly
Reviewed-by: dfuchs, chegar, michaelm
1 parent 83abfa5 commit 800e68d

File tree

7 files changed

+616
-9
lines changed

7 files changed

+616
-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
@@ -27,6 +27,7 @@
2727

2828
import java.io.IOException;
2929
import java.net.InetSocketAddress;
30+
import java.net.ProtocolException;
3031
import java.net.ProxySelector;
3132
import java.net.URI;
3233
import java.net.URISyntaxException;
@@ -472,10 +473,62 @@ private CompletableFuture<Response> sendRequestBody(ExchangeImpl<T> ex) {
472473
CompletableFuture<Response> cf = ex.sendBodyAsync()
473474
.thenCompose(exIm -> exIm.getResponseAsync(parentExecutor));
474475
cf = wrapForUpgrade(cf);
476+
// after 101 is handled we check for other 1xx responses
477+
cf = cf.thenCompose(this::ignore1xxResponse);
475478
cf = wrapForLog(cf);
476479
return cf;
477480
}
478481

482+
/**
483+
* Checks whether the passed Response has a status code between 102 and 199 (both inclusive).
484+
* If so, then that {@code Response} is considered intermediate informational response and is
485+
* ignored by the client. This method then creates a new {@link CompletableFuture} which
486+
* completes when a subsequent response is sent by the server. Such newly constructed
487+
* {@link CompletableFuture} will not complete till a "final" response (one which doesn't have
488+
* a response code between 102 and 199 inclusive) is sent by the server. The returned
489+
* {@link CompletableFuture} is thus capable of handling multiple subsequent intermediate
490+
* informational responses from the server.
491+
* <p>
492+
* If the passed Response doesn't have a status code between 102 and 199 (both inclusive) then
493+
* this method immediately returns back a completed {@link CompletableFuture} with the passed
494+
* {@code Response}.
495+
* </p>
496+
*
497+
* @param rsp The response
498+
* @return A {@code CompletableFuture} with the final response from the server
499+
*/
500+
private CompletableFuture<Response> ignore1xxResponse(final Response rsp) {
501+
final int statusCode = rsp.statusCode();
502+
// we ignore any response code which is 1xx.
503+
// For 100 (with the request configured to expect-continue) and 101, we handle it
504+
// specifically as defined in the RFC-9110, outside of this method.
505+
// As noted in RFC-9110, section 15.2.1, if response code is 100 and if the request wasn't
506+
// configured with expectContinue, then we ignore the 100 response and wait for the final
507+
// response (just like any other 1xx response).
508+
// Any other response code between 102 and 199 (both inclusive) aren't specified in the
509+
// "HTTP semantics" RFC-9110. The spec states that these 1xx response codes are informational
510+
// and interim and the client can choose to ignore them and continue to wait for the
511+
// final response (headers)
512+
if ((statusCode >= 102 && statusCode <= 199)
513+
|| (statusCode == 100 && !request.expectContinue)) {
514+
Log.logTrace("Ignoring (1xx informational) response code {0}", rsp.statusCode());
515+
if (debug.on()) {
516+
debug.log("Ignoring (1xx informational) response code "
517+
+ rsp.statusCode());
518+
}
519+
assert exchImpl != null : "Illegal state - current exchange isn't set";
520+
// ignore this Response and wait again for the subsequent response headers
521+
final CompletableFuture<Response> cf = exchImpl.getResponseAsync(parentExecutor);
522+
// we recompose the CF again into the ignore1xxResponse check/function because
523+
// the 1xx response is allowed to be sent multiple times for a request, before
524+
// a final response arrives
525+
return cf.thenCompose(this::ignore1xxResponse);
526+
} else {
527+
// return the already completed future
528+
return MinimalFuture.completedFuture(rsp);
529+
}
530+
}
531+
479532
CompletableFuture<Response> responseAsyncImpl0(HttpConnection connection) {
480533
Function<ExchangeImpl<T>, CompletableFuture<Response>> after407Check;
481534
bodyIgnored = null;
@@ -506,7 +559,30 @@ private CompletableFuture<Response> wrapForUpgrade(CompletableFuture<Response> c
506559
if (upgrading) {
507560
return cf.thenCompose(r -> checkForUpgradeAsync(r, exchImpl));
508561
}
509-
return cf;
562+
// websocket requests use "Connection: Upgrade" and "Upgrade: websocket" headers.
563+
// however, the "upgrading" flag we maintain in this class only tracks a h2 upgrade
564+
// that we internally triggered. So it will be false in the case of websocket upgrade, hence
565+
// this additional check. If it's a websocket request we allow 101 responses and we don't
566+
// require any additional checks when a response arrives.
567+
if (request.isWebSocket()) {
568+
return cf;
569+
}
570+
// not expecting an upgrade, but if the server sends a 101 response then we fail the
571+
// request and also let the ExchangeImpl deal with it as a protocol error
572+
return cf.thenCompose(r -> {
573+
if (r.statusCode == 101) {
574+
final ProtocolException protoEx = new ProtocolException("Unexpected 101 " +
575+
"response, when not upgrading");
576+
assert exchImpl != null : "Illegal state - current exchange isn't set";
577+
try {
578+
exchImpl.onProtocolError(protoEx);
579+
} catch (Throwable ignore){
580+
// ignored
581+
}
582+
return MinimalFuture.failedFuture(protoEx);
583+
}
584+
return MinimalFuture.completedFuture(r);
585+
});
510586
}
511587

512588
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
@@ -216,6 +216,16 @@ HttpBodySubscriberWrapper<T> createResponseSubscriber(HttpResponse.BodyHandler<T
216216
*/
217217
abstract void cancel(IOException cause);
218218

219+
/**
220+
* Invoked whenever there is a (HTTP) protocol error when dealing with the response
221+
* from the server. The implementations of {@code ExchangeImpl} are then expected to
222+
* take necessary action that is expected by the corresponding specifications whenever
223+
* a protocol error happens. For example, in HTTP/1.1, such protocol error would result
224+
* in the connection being closed.
225+
* @param cause The cause of the protocol violation
226+
*/
227+
abstract void onProtocolError(IOException cause);
228+
219229
/**
220230
* Called when the exchange is released, so that cleanup actions may be
221231
* 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
@@ -502,6 +502,15 @@ void cancel(IOException cause) {
502502
cancelImpl(cause);
503503
}
504504

505+
@Override
506+
void onProtocolError(final IOException cause) {
507+
if (debug.on()) {
508+
debug.log("cancelling exchange due to protocol error: %s", cause.getMessage());
509+
}
510+
Log.logError("cancelling exchange due to protocol error: {0}\n", cause);
511+
cancelImpl(cause);
512+
}
513+
505514
private void cancelImpl(Throwable cause) {
506515
LinkedList<CompletableFuture<?>> toComplete = null;
507516
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
@@ -346,7 +346,7 @@ private Http2Connection(HttpConnection connection,
346346
sendConnectionPreface();
347347
if (!opened) {
348348
debug.log("ensure reset frame is sent to cancel initial stream");
349-
initialStream.sendCancelStreamFrame();
349+
initialStream.sendResetStreamFrame(ResetFrame.CANCEL);
350350
}
351351

352352
}

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
@@ -37,6 +37,7 @@
3737
import java.net.ConnectException;
3838
import java.net.CookieHandler;
3939
import java.net.InetAddress;
40+
import java.net.ProtocolException;
4041
import java.net.ProxySelector;
4142
import java.net.http.HttpConnectTimeoutException;
4243
import java.net.http.HttpTimeoutException;
@@ -859,6 +860,8 @@ private void debugCompleted(String tag, long startNanos, HttpRequest req) {
859860
// any other SSLException is wrapped in a plain
860861
// SSLException
861862
throw new SSLException(msg, throwable);
863+
} else if (throwable instanceof ProtocolException) {
864+
throw new ProtocolException(msg);
862865
} else if (throwable instanceof IOException) {
863866
throw new IOException(msg, throwable);
864867
} 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
@@ -449,7 +449,7 @@ CompletableFuture<ExchangeImpl<T>> sendBodyAsync() {
449449
private boolean checkRequestCancelled() {
450450
if (exchange.multi.requestCancelled()) {
451451
if (errorRef.get() == null) cancel();
452-
else sendCancelStreamFrame();
452+
else sendResetStreamFrame(ResetFrame.CANCEL);
453453
return true;
454454
}
455455
return false;
@@ -1238,6 +1238,16 @@ void cancel(IOException cause) {
12381238
cancelImpl(cause);
12391239
}
12401240

1241+
@Override
1242+
void onProtocolError(final IOException cause) {
1243+
if (debug.on()) {
1244+
debug.log("cancelling exchange on stream %d due to protocol error: %s", streamid, cause.getMessage());
1245+
}
1246+
Log.logError("cancelling exchange on stream {0} due to protocol error: {1}\n", streamid, cause);
1247+
// send a RESET frame and close the stream
1248+
cancelImpl(cause, ResetFrame.PROTOCOL_ERROR);
1249+
}
1250+
12411251
void connectionClosing(Throwable cause) {
12421252
Flow.Subscriber<?> subscriber =
12431253
responseSubscriber == null ? pendingResponseSubscriber : responseSubscriber;
@@ -1249,6 +1259,10 @@ void connectionClosing(Throwable cause) {
12491259

12501260
// This method sends a RST_STREAM frame
12511261
void cancelImpl(Throwable e) {
1262+
cancelImpl(e, ResetFrame.CANCEL);
1263+
}
1264+
1265+
private void cancelImpl(final Throwable e, final int resetFrameErrCode) {
12521266
errorRef.compareAndSet(null, e);
12531267
if (debug.on()) {
12541268
if (streamid == 0) debug.log("cancelling stream: %s", (Object)e);
@@ -1280,25 +1294,25 @@ void cancelImpl(Throwable e) {
12801294
try {
12811295
// will send a RST_STREAM frame
12821296
if (streamid != 0 && streamState == 0) {
1283-
e = Utils.getCompletionCause(e);
1284-
if (e instanceof EOFException) {
1297+
final Throwable cause = Utils.getCompletionCause(e);
1298+
if (cause instanceof EOFException) {
12851299
// read EOF: no need to try & send reset
12861300
connection.decrementStreamsCount(streamid);
12871301
connection.closeStream(streamid);
12881302
} else {
12891303
// no use to send CANCEL if already closed.
1290-
sendCancelStreamFrame();
1304+
sendResetStreamFrame(resetFrameErrCode);
12911305
}
12921306
}
12931307
} catch (Throwable ex) {
12941308
Log.logError(ex);
12951309
}
12961310
}
12971311

1298-
void sendCancelStreamFrame() {
1312+
void sendResetStreamFrame(final int resetFrameErrCode) {
12991313
// do not reset a stream until it has a streamid.
1300-
if (streamid > 0 && markStream(ResetFrame.CANCEL) == 0) {
1301-
connection.resetStream(streamid, ResetFrame.CANCEL);
1314+
if (streamid > 0 && markStream(resetFrameErrCode) == 0) {
1315+
connection.resetStream(streamid, resetFrameErrCode);
13021316
}
13031317
close();
13041318
}

0 commit comments

Comments
 (0)