apache/seatunnel · error · java.net.ConnectException

Server response ${reply}

Error message

Server response ${reply}

What it means

The FTP file system's connect() opened a TCP socket to the configured host:port, but the server's first reply code was not a positive completion (not 1xx/2xx/3xx per FTPReply.isPositiveCompletion). The connector wraps the raw reply code in a ConnectException via NetUtils.wrapException, so the message looks like 'Server response 421' (or 530, 500, etc.). The socket connected, but the FTP service refused to proceed — e.g. server overloaded, TLS required, or the port is not actually an FTP endpoint.

Source

Thrown at seatunnel-connectors-v2/connector-file/connector-file-ftp/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/ftp/system/SeaTunnelFTPFileSystem.java:165

        // Check if remote verification is enabled
        boolean remoteVerificationEnabled =
                conf.getBoolean(
                        FS_FTP_REMOTE_VERIFICATION_ENABLED,
                        FtpFileBaseOptions.FTP_REMOTE_VERIFICATION_ENABLED.defaultValue());
        client.setRemoteVerificationEnabled(remoteVerificationEnabled);

        // Retrieve host, port, user, and password from configuration
        String host = conf.get(FS_FTP_HOST);
        int port = conf.getInt(FS_FTP_HOST_PORT, FTP.DEFAULT_PORT);
        String user = conf.get(FS_FTP_USER_PREFIX + host);
        String password = conf.get(FS_FTP_PASSWORD_PREFIX + host);

        // Connect to the FTP server
        client.connect(host, port);
        int reply = client.getReplyCode();
        if (!FTPReply.isPositiveCompletion(reply)) {
            throw NetUtils.wrapException(
                    host,
                    port,
                    NetUtils.UNKNOWN_HOST,
                    0,
                    new ConnectException("Server response " + reply));
        }

        // Log in to the FTP server
        if (!client.login(user, password)) {
            throw new IOException(
                    String.format(
                            "Login failed on server - %s, port - %d as user '%s', reply code: %d",
                            host, port, user, client.getReplyCode()));
        }

        // Set the file type to binary and buffer size
        client.setFileType(FTP.BINARY_FILE_TYPE);
        client.setBufferSize(DEFAULT_BUFFER_SIZE);

View on GitHub (pinned to cf67b549a7)

Solutions

  1. Read the reply code in the message: 421 = server busy/max connections (retry later or raise server connection limit); 530/needs-TLS = switch to an FTPS-capable setup or enable TLS; 500/502 = you are not talking to an FTP server (check host/port).
  2. Verify the host and port config (fs.ftp.host, fs.ftp.host-port) point at a plain FTP (or explicit-FTPS) endpoint — test with 'ftp host port' or a client like FileZilla and compare the banner.
  3. If the server requires FTPS, this SeaTunnelFTPFileSystem uses commons-net FTPClient without TLS — use an FTP server that allows plain FTP, or front it with a plain-FTP proxy/stunnel.
  4. Check server-side limits and health (max clients, IP allowlist, fail2ban) and retry with backoff on transient 421 codes.
  5. Confirm firewall/security-group rules permit the data-channel ports too, since some servers reject control sessions when the data channel cannot be established.

Example fix

// before: wrong port (SFTP server) -> Server response 500
String host = "sftp.example.com";
conf.set(FS_FTP_HOST, host);
conf.setInt(FS_FTP_HOST_PORT, 22); // SFTP, not FTP

// after: point at the real FTP service port
conf.set(FS_FTP_HOST, "ftp.example.com");
conf.setInt(FS_FTP_HOST_PORT, 21); // standard FTP control port
Defensive patterns

Strategy: retry

Validate before calling

// Validate reachability and banner before submitting the job
try (Socket s = new Socket()) {
    s.connect(new InetSocketAddress(host, port), 5000);
    BufferedReader in = new BufferedReader(new InputStreamReader(s.getInputStream()));
    String banner = in.readLine(); // e.g. "220 ftp.example.com FTP server ready"
    if (banner == null || !banner.startsWith("220")) {
        throw new IllegalStateException("Endpoint does not look like a healthy FTP server: " + banner);
    }
} catch (java.net.UnknownHostException | java.net.SocketTimeoutException e) {
    throw new IllegalStateException("Cannot reach FTP host/port: " + host + ":" + port, e);
}

Try / catch

int attempts = 0;
while (true) {
    try {
        ftpFileSystem.list(...); // triggers connect()
        break;
    } catch (IOException e) {
        Matcher m = Pattern.compile("Server response (\\d+)").matcher(e.getMessage());
        if (m.find()) {
            int reply = Integer.parseInt(m.group(1));
            if (reply == 421 && ++attempts <= 3) { // transient: server busy
                Thread.sleep(2000L * attempts);
                continue;
            }
        }
        throw e; // 530/500 etc. are configuration errors, do not retry
    }
}

Prevention

When it happens

Trigger: SeaTunnelFTPFileSystem.connect() — triggered on client(), openFileStatusListingSession(), getHomeDirectory() — when client.connect(host, port) succeeds at TCP level but client.getReplyCode() returns a non-positive-completion code: 421 (service not available / too many connections), 530 (not logged in / TLS required before commands), 500/502 (not an FTP server on that port), 120 (service ready in N minutes).

Common situations: Connecting to an FTPS-only server that requires AUTH TLS before serving commands; hitting a max-connections limit (421); pointing fs.ftp.host at the wrong port or at an SFTP/HTTP service (500); firewall/proxy allowing TCP connect but the FTP daemon rejecting; server under load during peak hours.

Related errors


AI-assisted analysis of apache/seatunnel@cf67b549a7 (2026-09-10). Data as JSON: /api/errors/9aaf1d765d59911e. Report an issue: GitHub.