2424import java .net .InetAddress ;
2525import java .net .InetSocketAddress ;
2626import java .net .ProxySelector ;
27- import java .net .SocketException ;
2827import java .net .URI ;
2928import java .net .URISyntaxException ;
3029import java .net .http .HttpClient ;
@@ -444,9 +443,13 @@ private static boolean isRetriableTransportFailure(IOException e) {
444443 if (Thread .currentThread ().isInterrupted ()) {
445444 return false ;
446445 }
447- // Walk the cause chain: the JDK HttpClient often wraps the underlying
448- // transport failure (e.g. SocketException "Connection reset") in a
449- // generic IOException ("HTTP/1.1 header parser received no bytes").
446+ // Walk the cause chain: the JDK HttpClient wraps the underlying transport
447+ // failure in different ways depending on the protocol path. Over HTTP/1.1
448+ // a "Connection reset" surfaces as a SocketException; over HTTP/2 (e.g.
449+ // a SETTINGS-frame write failure) it may surface as a plain IOException
450+ // with the same message. Match by message regardless of class to handle
451+ // both, while keeping the type checks for connect-phase timeouts and
452+ // request-phase timeouts (which must NEVER be retried).
450453 Throwable t = e ;
451454 while (t != null ) {
452455 if (t instanceof HttpConnectTimeoutException ) {
@@ -458,10 +461,10 @@ private static boolean isRetriableTransportFailure(IOException e) {
458461 if (t instanceof ConnectException ) {
459462 return true ;
460463 }
461- if ( t instanceof SocketException ) {
462- String msg = t . getMessage ();
463- return msg != null
464- && ( msg . contains ( "Connection reset" ) || msg . contains ( "Broken pipe" )) ;
464+ String msg = t . getMessage ();
465+ if ( msg != null
466+ && ( msg . contains ( "Connection reset" ) || msg . contains ( "Broken pipe" ))) {
467+ return true ;
465468 }
466469 t = t .getCause ();
467470 }
0 commit comments