apache/seatunnel · error · IOException
FTP LIST failed for path=${mask(absolute)}, replyCode=${clie
Error message
FTP LIST failed for path=${mask(absolute)}, replyCode=${client.getReplyCode()} What it means
Thrown by SeaTunnelFTPFileSystem.list() when initiateListParsing() completes but the FTP reply code is not a positive completion (2xx), i.e. the LIST command itself failed even though the CWD succeeded. The path is masked in the message and the raw reply code is included for diagnosis.
Source
Thrown at seatunnel-connectors-v2/connector-file/connector-file-ftp/src/main/java/org/apache/seatunnel/connectors/seatunnel/file/ftp/system/SeaTunnelFTPFileSystem.java:538
@Override
public FileStatus getFileStatus(Path path) throws IOException {
return SeaTunnelFTPFileSystem.this.getFileStatus(client, path);
}
@Override
public void list(Path directory, FileStatusConsumer consumer) throws IOException {
Path workDir = new Path(client.printWorkingDirectory());
Path absolute = makeAbsolute(workDir, directory);
String previousDirectory = client.printWorkingDirectory();
if (!client.changeWorkingDirectory(absolute.toUri().getPath())) {
throw new FileNotFoundException("FTP directory does not exist: " + mask(absolute));
}
Throwable failure = null;
try {
FTPListParseEngine engine = client.initiateListParsing();
if (!FTPReply.isPositiveCompletion(client.getReplyCode())) {
throw new IOException(
"FTP LIST failed for path="
+ mask(absolute)
+ ", replyCode="
+ client.getReplyCode());
}
while (engine.hasNext()) {
FTPFile[] files = engine.getNext(PAGE_SIZE);
int skipped = emitFileStatuses(files, absolute, consumer);
if (skipped > 0) {
LOG.warn(
"Skipped "
+ skipped
+ " unparseable FTP listing entries under "
+ mask(absolute));
}
}
} catch (IOException | RuntimeException | Error e) {
failure = e;View on GitHub (pinned to cf67b549a7)
Solutions
- Check the reply code: 421 → connection limit, 425/426 → data channel/network, 5xx → command rejected
- Switch to passive mode and open the required passive port range on the firewall
- Reduce job parallelism to stay under the FTP server's max connection limit
- Retry the listing on transient reply codes (4xx)
- Test LIST behavior manually with the same credentials via an FTP CLI client
Example fix
// before
// no retry, single list attempt
FileStatus[] files = fs.listStatus(dir);
// after
// retry transient FTP failures (4xx) with backoff before failing the job
for (int attempt = 0; attempt < 3; attempt++) {
try { return fs.listStatus(dir); } catch (IOException e) { sleep(backoff(attempt)); }
} Defensive patterns
Strategy: retry
Validate before calling
// verify LIST works before the job: open a test connection and list the base dir // also confirm passive mode + open data ports in the firewall
Try / catch
for (int attempt = 1; attempt <= 3; attempt++) {
try { return fs.listStatus(dir); }
catch (IOException e) {
if (e.getMessage().contains("FTP LIST failed") && attempt < 3) { backoffAndRetry(); }
else throw e;
}
} Prevention
- Enable passive mode and allow the passive port range through firewalls/NAT
- Keep job parallelism under the FTP server's max-connection limit
- Retry on transient 4xx reply codes
- Validate LIST behavior manually with the same user before production runs
When it happens
Trigger: The FTP data channel for LIST could not be established or the server rejected LIST — passive-mode blocked by firewall/NAT, connection limit reached (421), transfer aborted (426), or server not supporting the default list parsing on that path.
Common situations: Corporate firewalls blocking the passive data port range; FTP server under max-connection limits due to high job parallelism; transient network drops during directory enumeration; misconfigured active/passive mode in the connector.
Understand the failure class
Background: 'Something went wrong' / 'Request failed (500)' / 'HTTP error! status: 404' — what failed HTTP requests actually mean and how to find the real cause — this error's family across 28 libraries.
Related errors
- Login failed on server - %s, port - %d as user '%s', reply c
- Unable to open file: ${file}, Aborting
- Client not connected
- Could not complete transfer, Reply Code - ${client.getReplyC
- FTP directory does not exist: ${mask(absolute)}
AI-assisted analysis of apache/seatunnel@cf67b549a7 (2026-09-10).
Data as JSON: /api/errors/f96461c34a93ce21.
Report an issue: GitHub.