Skip to content

Commit 73dc9e8

Browse files
committed
8292044: HttpClient doesn't handle 102 or 103 properly
Reviewed-by: mdoerr Backport-of: 10000286390ac9b0288cee25a4f3551d09475fdc
1 parent 648e8f0 commit 73dc9e8

File tree

7 files changed

+617
-8
lines changed

7 files changed

+617
-8
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;
@@ -410,10 +411,62 @@ private CompletableFuture<Response> sendRequestBody(ExchangeImpl<T> ex) {
410411
CompletableFuture<Response> cf = ex.sendBodyAsync()
411412
.thenCompose(exIm -> exIm.getResponseAsync(parentExecutor));
412413
cf = wrapForUpgrade(cf);
414+
// after 101 is handled we check for other 1xx responses
415+
cf = cf.thenCompose(this::ignore1xxResponse);
413416
cf = wrapForLog(cf);
414417
return cf;
415418
}
416419

420+
/**
421+
* Checks whether the passed Response has a status code between 102 and 199 (both inclusive).
422+
* If so, then that {@code Response} is considered intermediate informational response and is
423+
* ignored by the client. This method then creates a new {@link CompletableFuture} which
424+
* completes when a subsequent response is sent by the server. Such newly constructed
425+
* {@link CompletableFuture} will not complete till a "final" response (one which doesn't have
426+
* a response code between 102 and 199 inclusive) is sent by the server. The returned
427+
* {@link CompletableFuture} is thus capable of handling multiple subsequent intermediate
428+
* informational responses from the server.
429+
* <p>
430+
* If the passed Response doesn't have a status code between 102 and 199 (both inclusive) then
431+
* this method immediately returns back a completed {@link CompletableFuture} with the passed
432+
* {@code Response}.
433+
* </p>
434+
*
435+
* @param rsp The response
436+
* @return A {@code CompletableFuture} with the final response from the server
437+
*/
438+
private CompletableFuture<Response> ignore1xxResponse(final Response rsp) {
439+
final int statusCode = rsp.statusCode();
440+
// we ignore any response code which is 1xx.
441+
// For 100 (with the request configured to expect-continue) and 101, we handle it
442+
// specifically as defined in the RFC-9110, outside of this method.
443+
// As noted in RFC-9110, section 15.2.1, if response code is 100 and if the request wasn't
444+
// configured with expectContinue, then we ignore the 100 response and wait for the final
445+
// response (just like any other 1xx response).
446+
// Any other response code between 102 and 199 (both inclusive) aren't specified in the
447+
// "HTTP semantics" RFC-9110. The spec states that these 1xx response codes are informational
448+
// and interim and the client can choose to ignore them and continue to wait for the
449+
// final response (headers)
450+
if ((statusCode >= 102 && statusCode <= 199)
451+
|| (statusCode == 100 && !request.expectContinue)) {
452+
Log.logTrace("Ignoring (1xx informational) response code {0}", rsp.statusCode());
453+
if (debug.on()) {
454+
debug.log("Ignoring (1xx informational) response code "
455+
+ rsp.statusCode());
456+
}
457+
assert exchImpl != null : "Illegal state - current exchange isn't set";
458+
// ignore this Response and wait again for the subsequent response headers
459+
final CompletableFuture<Response> cf = exchImpl.getResponseAsync(parentExecutor);
460+
// we recompose the CF again into the ignore1xxResponse check/function because
461+
// the 1xx response is allowed to be sent multiple times for a request, before
462+
// a final response arrives
463+
return cf.thenCompose(this::ignore1xxResponse);
464+
} else {
465+
// return the already completed future
466+
return MinimalFuture.completedFuture(rsp);
467+
}
468+
}
469+
417470
CompletableFuture<Response> responseAsyncImpl0(HttpConnection connection) {
418471
Function<ExchangeImpl<T>, CompletableFuture<Response>> after407Check;
419472
bodyIgnored = null;
@@ -444,7 +497,30 @@ private CompletableFuture<Response> wrapForUpgrade(CompletableFuture<Response> c
444497
if (upgrading) {
445498
return cf.thenCompose(r -> checkForUpgradeAsync(r, exchImpl));
446499
}
447-
return cf;
500+
// websocket requests use "Connection: Upgrade" and "Upgrade: websocket" headers.
501+
// however, the "upgrading" flag we maintain in this class only tracks a h2 upgrade
502+
// that we internally triggered. So it will be false in the case of websocket upgrade, hence
503+
// this additional check. If it's a websocket request we allow 101 responses and we don't
504+
// require any additional checks when a response arrives.
505+
if (request.isWebSocket()) {
506+
return cf;
507+
}
508+
// not expecting an upgrade, but if the server sends a 101 response then we fail the
509+
// request and also let the ExchangeImpl deal with it as a protocol error
510+
return cf.thenCompose(r -> {
511+
if (r.statusCode == 101) {
512+
final ProtocolException protoEx = new ProtocolException("Unexpected 101 " +
513+
"response, when not upgrading");
514+
assert exchImpl != null : "Illegal state - current exchange isn't set";
515+
try {
516+
exchImpl.onProtocolError(protoEx);
517+
} catch (Throwable ignore){
518+
// ignored
519+
}
520+
return MinimalFuture.failedFuture(protoEx);
521+
}
522+
return MinimalFuture.completedFuture(r);
523+
});
448524
}
449525

450526
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
@@ -426,6 +426,15 @@ void cancel(IOException cause) {
426426
cancelImpl(cause);
427427
}
428428

429+
@Override
430+
void onProtocolError(final IOException cause) {
431+
if (debug.on()) {
432+
debug.log("cancelling exchange due to protocol error: %s", cause.getMessage());
433+
}
434+
Log.logError("cancelling exchange due to protocol error: {0}\n", cause);
435+
cancelImpl(cause);
436+
}
437+
429438
private void cancelImpl(Throwable cause) {
430439
LinkedList<CompletableFuture<?>> toComplete = null;
431440
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
@@ -34,6 +34,7 @@
3434
import java.net.Authenticator;
3535
import java.net.ConnectException;
3636
import java.net.CookieHandler;
37+
import java.net.ProtocolException;
3738
import java.net.ProxySelector;
3839
import java.net.http.HttpConnectTimeoutException;
3940
import java.net.http.HttpTimeoutException;
@@ -561,6 +562,8 @@ private void debugCompleted(String tag, long startNanos, HttpRequest req) {
561562
ConnectException ce = new ConnectException(msg);
562563
ce.initCause(throwable);
563564
throw ce;
565+
} else if (throwable instanceof ProtocolException) {
566+
throw new ProtocolException(msg);
564567
} else if (throwable instanceof IOException) {
565568
throw new IOException(msg, throwable);
566569
} else {

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

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1213,6 +1213,16 @@ void cancel(IOException cause) {
12131213
cancelImpl(cause);
12141214
}
12151215

1216+
@Override
1217+
void onProtocolError(final IOException cause) {
1218+
if (debug.on()) {
1219+
debug.log("cancelling exchange on stream %d due to protocol error: %s", streamid, cause.getMessage());
1220+
}
1221+
Log.logError("cancelling exchange on stream {0} due to protocol error: {1}\n", streamid, cause);
1222+
// send a RESET frame and close the stream
1223+
cancelImpl(cause, ResetFrame.PROTOCOL_ERROR);
1224+
}
1225+
12161226
void connectionClosing(Throwable cause) {
12171227
Flow.Subscriber<?> subscriber =
12181228
responseSubscriber == null ? pendingResponseSubscriber : responseSubscriber;
@@ -1224,6 +1234,10 @@ void connectionClosing(Throwable cause) {
12241234

12251235
// This method sends a RST_STREAM frame
12261236
void cancelImpl(Throwable e) {
1237+
cancelImpl(e, ResetFrame.CANCEL);
1238+
}
1239+
1240+
private void cancelImpl(final Throwable e, final int resetFrameErrCode) {
12271241
errorRef.compareAndSet(null, e);
12281242
if (debug.on()) {
12291243
if (streamid == 0) debug.log("cancelling stream: %s", (Object)e);
@@ -1255,25 +1269,25 @@ void cancelImpl(Throwable e) {
12551269
try {
12561270
// will send a RST_STREAM frame
12571271
if (streamid != 0 && streamState == 0) {
1258-
e = Utils.getCompletionCause(e);
1259-
if (e instanceof EOFException) {
1272+
final Throwable cause = Utils.getCompletionCause(e);
1273+
if (cause instanceof EOFException) {
12601274
// read EOF: no need to try & send reset
12611275
connection.decrementStreamsCount(streamid);
12621276
connection.closeStream(streamid);
12631277
} else {
12641278
// no use to send CANCEL if already closed.
1265-
sendCancelStreamFrame();
1279+
sendResetStreamFrame(resetFrameErrCode);
12661280
}
12671281
}
12681282
} catch (Throwable ex) {
12691283
Log.logError(ex);
12701284
}
12711285
}
12721286

1273-
void sendCancelStreamFrame() {
1287+
void sendResetStreamFrame(final int resetFrameErrCode) {
12741288
// do not reset a stream until it has a streamid.
1275-
if (streamid > 0 && markStream(ResetFrame.CANCEL) == 0) {
1276-
connection.resetStream(streamid, ResetFrame.CANCEL);
1289+
if (streamid > 0 && markStream(resetFrameErrCode) == 0) {
1290+
connection.resetStream(streamid, resetFrameErrCode);
12771291
}
12781292
close();
12791293
}

0 commit comments

Comments
 (0)