@@ -147,7 +147,9 @@ private HttpResponse sendPatchRequestWithBackoff(String relativeUrl, HttpContent
147147 private HttpResponse sendPatchRequest (
148148 String relativeUrl , HttpContent content , BackoffTimer backoffTimer ) throws DeepLException {
149149 // Like HttpURLConnection, honor the JVM's proxy settings unless a proxy was configured.
150- HttpClientBuilder builder = HttpClients .custom ().useSystemProperties ();
150+ // Automatic retries are disabled so that only the backoff loop retries, up to maxRetries.
151+ HttpClientBuilder builder =
152+ HttpClients .custom ().useSystemProperties ().disableAutomaticRetries ();
151153 if (proxy != null ) {
152154 InetSocketAddress addr = (InetSocketAddress ) proxy .address ();
153155 builder .setProxy (new HttpHost (addr .getHostName (), addr .getPort ()));
@@ -328,7 +330,7 @@ private HttpResponseStream sendRequest(
328330 } catch (IOException e ) {
329331 ConnectionException failure = toConnectionException (e );
330332 throw new ConnectionException (
331- failure .getMessage (), failure .getShouldRetry (), e , failure . getShouldRetry ( ));
333+ failure .getMessage (), failure .getShouldRetry (), e , isUnsentFailure ( e ));
332334 }
333335
334336 if (content != null ) {
@@ -356,14 +358,50 @@ private HttpResponseStream sendRequest(
356358 }
357359 }
358360
359- // Connect, DNS and timeout failures are transient: the request is retried.
361+ /**
362+ * Copies a streamed response body to the output stream. A failure while reading the body is a
363+ * {@link ConnectionException}; a failure while writing the output stays an {@link IOException}.
364+ */
365+ static void copyBody (InputStream body , OutputStream outputStream )
366+ throws ConnectionException , IOException {
367+ final byte [] buffer = new byte [8192 ];
368+ while (true ) {
369+ int read ;
370+ try {
371+ read = body .read (buffer );
372+ } catch (RuntimeException | IOException e ) {
373+ throw toConnectionException (e );
374+ }
375+ if (read < 0 ) {
376+ return ;
377+ }
378+ outputStream .write (buffer , 0 , read );
379+ }
380+ }
381+
382+ // Connect, DNS, timeout, connection reset and broken pipe failures are transient: the request is
383+ // retried.
360384 private static ConnectionException toConnectionException (Exception e ) {
361- boolean shouldRetry =
362- e instanceof SocketTimeoutException
363- || e instanceof ConnectException
364- || e instanceof NoRouteToHostException
365- || e instanceof UnknownHostException
366- || e instanceof ConnectTimeoutException ;
385+ boolean shouldRetry = isUnsentFailure (e ) || isConnectionResetOrBrokenPipe (e );
367386 return new ConnectionException (e .getMessage (), shouldRetry , e );
368387 }
388+
389+ // Failures of the connect step after which the server cannot have received the request.
390+ private static boolean isUnsentFailure (Exception e ) {
391+ return e instanceof SocketTimeoutException
392+ || e instanceof ConnectException
393+ || e instanceof NoRouteToHostException
394+ || e instanceof UnknownHostException
395+ || e instanceof ConnectTimeoutException ;
396+ }
397+
398+ // Java has no exception types for these, only SocketException messages such as
399+ // "Connection reset", "Connection reset by peer" and "Broken pipe (Write failed)".
400+ private static boolean isConnectionResetOrBrokenPipe (Exception e ) {
401+ if (!(e instanceof SocketException ) || e .getMessage () == null ) {
402+ return false ;
403+ }
404+ String message = e .getMessage ().toLowerCase (Locale .ROOT );
405+ return message .contains ("connection reset" ) || message .contains ("broken pipe" );
406+ }
369407}
0 commit comments