1818import java .io .InputStream ;
1919import java .net .ConnectException ;
2020import java .net .ProxySelector ;
21- import java .net .SocketException ;
2221import java .net .URI ;
2322import java .net .URISyntaxException ;
2423import java .net .http .HttpClient ;
@@ -407,9 +406,13 @@ private static boolean isRetriableTransportFailure(IOException e) {
407406 if (Thread .currentThread ().isInterrupted ()) {
408407 return false ;
409408 }
410- // Walk the cause chain: the JDK HttpClient often wraps the underlying
411- // transport failure (e.g. SocketException "Connection reset") in a
412- // generic IOException ("HTTP/1.1 header parser received no bytes").
409+ // Walk the cause chain: the JDK HttpClient wraps the underlying transport
410+ // failure in different ways depending on the protocol path. Over HTTP/1.1
411+ // a "Connection reset" surfaces as a SocketException; over HTTP/2 (e.g.
412+ // a SETTINGS-frame write failure) it may surface as a plain IOException
413+ // with the same message. Match by message regardless of class to handle
414+ // both, while keeping the type checks for connect-phase timeouts and
415+ // request-phase timeouts (which must NEVER be retried).
413416 Throwable t = e ;
414417 while (t != null ) {
415418 if (t instanceof HttpConnectTimeoutException ) {
@@ -421,10 +424,10 @@ private static boolean isRetriableTransportFailure(IOException e) {
421424 if (t instanceof ConnectException ) {
422425 return true ;
423426 }
424- if ( t instanceof SocketException ) {
425- String msg = t . getMessage ();
426- return msg != null
427- && ( msg . contains ( "Connection reset" ) || msg . contains ( "Broken pipe" )) ;
427+ String msg = t . getMessage ();
428+ if ( msg != null
429+ && ( msg . contains ( "Connection reset" ) || msg . contains ( "Broken pipe" ))) {
430+ return true ;
428431 }
429432 t = t .getCause ();
430433 }
0 commit comments