-
Notifications
You must be signed in to change notification settings - Fork 177
fix: retry a request when establishing the connection timed out #3170
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| /* Copyright 2026 predic8 GmbH, www.predic8.com | ||
|
|
||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||
| you may not use this file except in compliance with the License. | ||
| You may obtain a copy of the License at | ||
|
|
||
| http://www.apache.org/licenses/LICENSE-2.0 | ||
|
|
||
| Unless required by applicable law or agreed to in writing, software | ||
| distributed under the License is distributed on an "AS IS" BASIS, | ||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| See the License for the specific language governing permissions and | ||
| limitations under the License. */ | ||
|
|
||
| package com.predic8.membrane.core.transport.http; | ||
|
|
||
| import java.net.SocketTimeoutException; | ||
|
|
||
| /** | ||
| * Indicates that establishing the connection to the target timed out, as opposed to the target | ||
| * accepting the connection but not answering in time. The JDK reports both as | ||
| * {@link SocketTimeoutException} and only distinguishes them by message text. | ||
| * <p> | ||
| * The distinction matters for retries: no byte of the request has been sent yet, so the target | ||
| * cannot have processed it and a retry is safe for any request method. | ||
| * <p> | ||
| * Extends {@link SocketTimeoutException} so that code catching the timeout in general keeps working. | ||
| */ | ||
| public class ConnectTimeoutException extends SocketTimeoutException { | ||
|
|
||
| private static final long serialVersionUID = 1L; | ||
|
|
||
| public ConnectTimeoutException(String message, Throwable cause) { | ||
| super(message); | ||
| initCause(cause); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -43,6 +43,8 @@ | |
| * <p>A retry is triggered for:</p> | ||
| * <ul> | ||
| * <li>Connection/IO exceptions (timeout, refused, reset...)</li> | ||
| * <li>A timeout while the connection was still being established (when | ||
| * {@code retryOnConnectTimeout=true}), for any request method</li> | ||
|
Comment on lines
+46
to
+47
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Use HTML code markup in generated configuration documentation.
As per coding guidelines, generated reference documentation must use HTML markup instead of 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| * <li>HTTP 408 Request Timeout</li> | ||
| * <li>HTTP 500, 502, 503, 504, 507 (when {@code failOverOn5XX=true})</li> | ||
| * </ul> | ||
|
|
@@ -74,6 +76,12 @@ public class RetryHandler { | |
| */ | ||
| private boolean failOverOn5XX = false; | ||
|
|
||
| /** | ||
| * Retry when establishing the connection timed out. Safe for any request method, because no part | ||
| * of the request was sent. Unlike a read timeout, this cannot have changed state on the server. | ||
| */ | ||
| private boolean retryOnConnectTimeout = true; | ||
|
|
||
| private static final Set<Integer> RETRYABLE_5XX = Set.of(500, 502, 503, 504, 507); | ||
|
|
||
| /** | ||
|
|
@@ -167,9 +175,17 @@ private boolean shouldAbortRetries(Exchange exc, Exception e, String dest, int a | |
| log.debug("Connection to {} refused.", dest); | ||
| return !hasMultipleNodes(exc); | ||
| } | ||
| // The socket read or connection took too long and exceeded the configured timeout. | ||
| // No data was received from the server in time. | ||
| // Causes: Server is overloaded, network latency or drop, TLS handshake took too long | ||
| // The connection was never established, so nothing was sent and no state was changed on the | ||
| // server. Retrying is safe for any method. Causes: dropped SYN, host unreachable, a TLS | ||
| // handshake that did not complete in time. Has to be checked before SocketTimeoutException, | ||
| // which it extends. | ||
| if (e instanceof ConnectTimeoutException) { | ||
| log.debug("Connection to {} timed out before it was established.", dest); | ||
| return !retryOnConnectTimeout; | ||
| } | ||
| // The socket read took too long and exceeded the configured timeout. No data was received | ||
| // from the server in time, but the request may already have been processed. | ||
| // Causes: Server is overloaded, network latency or drop | ||
| if (e instanceof SocketTimeoutException) { | ||
| log.debug("Connection to {} timed out.", dest); | ||
| return !isIdempotent(exc.getRequest().getMethod()) || !hasMultipleNodes(exc); | ||
|
|
@@ -303,6 +319,23 @@ public void setFailOverOn5XX(boolean failOverOn5XX) { | |
| this.failOverOn5XX = failOverOn5XX; | ||
| } | ||
|
|
||
| public boolean isRetryOnConnectTimeout() { | ||
| return retryOnConnectTimeout; | ||
| } | ||
|
|
||
| /** | ||
| * @description If <code>true</code> retry when the connection to the target could not be | ||
| * established within the connection timeout. No part of the request has been sent in | ||
| * that case, so this applies to every request method, including POST and PATCH. A | ||
| * timeout while reading the response is not covered by this and stays restricted to | ||
| * idempotent methods. Set to <code>false</code> to fail fast instead. | ||
| * @default true | ||
| */ | ||
| @MCAttribute | ||
| public void setRetryOnConnectTimeout(boolean retryOnConnectTimeout) { | ||
| this.retryOnConnectTimeout = retryOnConnectTimeout; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean equals(Object o) { | ||
| if (o == null || getClass() != o.getClass()) return false; | ||
|
|
@@ -311,7 +344,8 @@ public boolean equals(Object o) { | |
| return retries == that.retries && | ||
| delay == that.delay && | ||
| Double.compare(backoffMultiplier, that.backoffMultiplier) == 0 && | ||
| Objects.equals(failOverOn5XX, that.failOverOn5XX); | ||
| Objects.equals(failOverOn5XX, that.failOverOn5XX) && | ||
| retryOnConnectTimeout == that.retryOnConnectTimeout; | ||
| } | ||
|
|
||
| @Override | ||
|
|
@@ -320,6 +354,7 @@ public int hashCode() { | |
| result = 31 * result + delay; | ||
| result = 31 * result + Double.hashCode(backoffMultiplier); | ||
| result = 31 * result + Boolean.hashCode(failOverOn5XX); | ||
| result = 31 * result + Boolean.hashCode(retryOnConnectTimeout); | ||
| return result; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.