This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 394b09aca966 CAMEL-23216: Fix flaky SFTP tests by verifying SSH
protocol readiness (#24601)
394b09aca966 is described below
commit 394b09aca966b26fe3649ee629e1ece5b1e91525
Author: Guillaume Nodet <[email protected]>
AuthorDate: Sat Jul 11 17:06:47 2026 +0200
CAMEL-23216: Fix flaky SFTP tests by verifying SSH protocol readiness
(#24601)
CAMEL-23216: Fix flaky SFTP tests by verifying SSH protocol readiness
Replace TCP-only readiness check with SSH banner verification (RFC 4253)
in SftpEmbeddedInfraService.waitForServerReady(). The embedded SSHD
server accepts TCP connections before the SSH protocol layer finishes
initializing, causing clients to time out during the SSH handshake —
the root cause of 591 flaky outcomes across 20+ test containers.
Changes:
- Verify SSH version banner (SSH-2.0-...) instead of just TCP connectivity
- Replace Thread.onSpinWait() busy-wait with Thread.sleep(100) backoff
- Use StandardCharsets.US_ASCII per RFC 4253 §4.2
- Make waitForServerReady() protected for subclass reuse
- Track last banner value for better timeout diagnostics
- Add waitForServerReady() call in SftpCertHostVerificationIT
Co-authored-by: Claude Opus 4.6 <[email protected]>
---
.../integration/SftpCertHostVerificationIT.java | 2 +
.../embedded/SftpEmbeddedInfraService.java | 49 +++++++++++++++++++---
2 files changed, 46 insertions(+), 5 deletions(-)
diff --git
a/components/camel-ftp/src/test/java/org/apache/camel/component/file/remote/sftp/integration/SftpCertHostVerificationIT.java
b/components/camel-ftp/src/test/java/org/apache/camel/component/file/remote/sftp/integration/SftpCertHostVerificationIT.java
index 839696ccef53..e3e246e67011 100644
---
a/components/camel-ftp/src/test/java/org/apache/camel/component/file/remote/sftp/integration/SftpCertHostVerificationIT.java
+++
b/components/camel-ftp/src/test/java/org/apache/camel/component/file/remote/sftp/integration/SftpCertHostVerificationIT.java
@@ -115,6 +115,8 @@ public class SftpCertHostVerificationIT extends
BaseServerTestSupport {
sshd.start();
port = sshd.getPort();
+
+ waitForServerReady();
}
};
diff --git
a/test-infra/camel-test-infra-ftp/src/main/java/org/apache/camel/test/infra/ftp/services/embedded/SftpEmbeddedInfraService.java
b/test-infra/camel-test-infra-ftp/src/main/java/org/apache/camel/test/infra/ftp/services/embedded/SftpEmbeddedInfraService.java
index 35b754ddd1cc..6ce220931b3c 100644
---
a/test-infra/camel-test-infra-ftp/src/main/java/org/apache/camel/test/infra/ftp/services/embedded/SftpEmbeddedInfraService.java
+++
b/test-infra/camel-test-infra-ftp/src/main/java/org/apache/camel/test/infra/ftp/services/embedded/SftpEmbeddedInfraService.java
@@ -21,6 +21,7 @@ import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -142,19 +143,57 @@ public class SftpEmbeddedInfraService extends
AbstractService implements FtpInfr
waitForServerReady();
}
- private void waitForServerReady() throws IOException {
+ /**
+ * Waits for the embedded SFTP server to be fully ready to accept SSH
connections.
+ * <p/>
+ * This method goes beyond a simple TCP port check — it verifies that the
SSH protocol layer is initialized by
+ * reading the server's SSH version banner (e.g., "SSH-2.0-..."). A
TCP-only check can pass while the SSH layer is
+ * still initializing, causing clients to time out during the SSH
handshake. This was the root cause of widespread
+ * flaky tests in camel-mina-sftp (CAMEL-23216).
+ * <p/>
+ * This method is {@code protected} so that subclasses that override
{@link #setUpServer()} can call it after
+ * starting their own {@link SshServer} instance.
+ *
+ * @throws IOException if the server does not become ready within 30
seconds
+ */
+ protected void waitForServerReady() throws IOException {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(30);
IOException lastException = null;
+ String lastBanner = null;
while (System.nanoTime() < deadline) {
try (Socket socket = new Socket()) {
- socket.connect(new InetSocketAddress("localhost", port), 1000);
- return;
+ socket.connect(new InetSocketAddress("localhost", port), 2000);
+ socket.setSoTimeout(5000);
+ // Read the SSH version banner to verify the SSH protocol
layer is ready.
+ // The server sends "SSH-2.0-..." immediately upon accepting a
connection.
+ // A successful TCP connect alone is not sufficient — the SSH
handshake can
+ // still time out if the protocol layer hasn't finished
initializing.
+ // SSH version strings are US-ASCII per RFC 4253 §4.2.
+ byte[] buf = new byte[256];
+ int len = socket.getInputStream().read(buf);
+ if (len > 0) {
+ lastBanner = new String(buf, 0, len,
StandardCharsets.US_ASCII).trim();
+ if (lastBanner.startsWith("SSH-")) {
+ LOG.debug("SFTP server ready on port {} (banner: {})",
port, lastBanner);
+ return;
+ }
+ }
+ // TCP connected but no SSH banner yet — server still
initializing
} catch (IOException e) {
lastException = e;
- Thread.onSpinWait();
+ }
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IOException("Interrupted while waiting for SFTP
server on port " + port, e);
}
}
- throw new IOException("SFTP server not ready after 30 seconds on port
" + port, lastException);
+ String detail = lastBanner != null
+ ? "last banner received: \"" + lastBanner + "\""
+ : "no banner received";
+ throw new IOException(
+ "SFTP server not ready after 30 seconds on port " + port + "
(" + detail + ")", lastException);
}
protected PublickeyAuthenticator getPublickeyAuthenticator() {