Skip to content

UploadStream returns failure (426) after successful upload; file exists on server (possible false negative) #1789

@mabellan

Description

@mabellan

Description
When calling UploadStream the method returns a failure due to a 426 Failure reading network stream response after the server has already stored the file. The file is present on the server with the correct size. This results in a false negative outcome from the API.
To mitigate this, I had to implement a workaround that, on failure, checks whether the file exists and matches the uploaded size.
This looks like a race/timing or control-channel handling issue: data transfer completes (150 Ok to send data + TLS data channel established) but the final control response is 426 even though the file is actually written.

Environment
Library: FluentFTP version 53.0.2
Runtime: .NET 8.0
Code running in: mcr.microsoft.com/dotnet/aspnet:8.0-alpine image
Protocol: FTPS (explicit) with TLS 1.3
FTP server: using docker image quay.io/aminvakil/alpine-ftp-server-tls:3.0.5-r2

Code
Here is the code I am using:

public async Task<bool> UploadStream(Stream stream, string path, CancellationToken token, ILogger? logger = null)
{
    _ftpClient.Config.LogToConsole = true;
    _ftpClient.Config.LogHost = true;

    long size = 0;
    if (stream.CanSeek)
    {
        size = stream.Seek(0, SeekOrigin.End);
        stream.Position = 0;
    }

    var result = await _ftpClient.UploadStream(
        stream,
        path,
        FtpRemoteExists.Overwrite,
        createRemoteDir: true,
        progress: null,
        token: token);

    // Workaround for false negative (see Details below)
    if (result == FtpStatus.Success)
    {
        logger?.LogInformation("File successfully uploaded to {Path}", path);
        return true;
    }

    var exists = await _ftpClient.FileExists(path, token);
    if (exists)
    {
        var uploadedSize = await _ftpClient.GetFileSize(path, token: token);
        logger?.LogWarning("UploadStream returned FAIL but file exists ({Size} bytes)", uploadedSize);
        return size == uploadedSize;
    }

    return false;
}

Relevant logs

2025-11-12T12:47:45.496626515Z # UploadStream("/ftp/user/sortReport20251112124745.csv", Overwrite, True)
2025-11-12T12:47:45.501836137Z 
2025-11-12T12:47:45.501898697Z # FileExists("/ftp/user/sortReport20251112124745.csv")
2025-11-12T12:47:45.501907184Z Command:  SIZE /ftp/user/sortReport20251112124745.csv
2025-11-12T12:47:45.502304750Z Status:   Waiting for response to: SIZE /ftp/user/sortReport20251112124745.csv
2025-11-12T12:47:45.511324684Z Response: 550 Could not get file size. [9ms]
2025-11-12T12:47:45.513367131Z 
2025-11-12T12:47:45.513395886Z # DirectoryExists("/ftp/user")
2025-11-12T12:47:45.513404342Z Command:  CWD /ftp/user
2025-11-12T12:47:45.513582265Z Status:   Waiting for response to: CWD /ftp/user
2025-11-12T12:47:45.514209864Z Response: 250 Directory successfully changed. [<1ms]
2025-11-12T12:47:45.514905835Z Command:  CWD /ftp/user
2025-11-12T12:47:45.514922648Z Status:   Waiting for response to: CWD /ftp/user
2025-11-12T12:47:45.515515660Z Response: 250 Directory successfully changed. [<1ms]
2025-11-12T12:47:45.516825264Z 
2025-11-12T12:47:45.516846194Z # OpenWrite("/ftp/user/sortReport20251112124745.csv", Binary, -1, False)
2025-11-12T12:47:45.516852286Z Command:  TYPE I
2025-11-12T12:47:45.516864079Z Status:   Waiting for response to: TYPE I
2025-11-12T12:47:45.517458815Z Response: 200 Switching to Binary mode. [<1ms]
2025-11-12T12:47:45.518574535Z 
2025-11-12T12:47:45.518596978Z # OpenDataStreamAsync("STOR /ftp/user/sortReport20251112124745.csv", 0)
2025-11-12T12:47:45.518605955Z 
2025-11-12T12:47:45.518613199Z # OpenPassiveDataStreamAsync(PASV, "STOR /ftp/user/sortReport20251112124745.csv", 0)
2025-11-12T12:47:45.518620503Z Command:  PASV
2025-11-12T12:47:45.518833634Z Status:   Waiting for response to: PASV
2025-11-12T12:47:45.519523303Z Response: 227 Entering Passive Mode (127,0,0,1,82,12). [<1ms]
2025-11-12T12:47:45.519538603Z Status:   PASV advertised a non-routable IPAD. Using original connect dnsname/IPAD
2025-11-12T12:47:45.519570484Z Status:   Connecting(async) AsyncFtpClient.FtpSocketStream(data) IP #1 = 192.168.100.4:21004
2025-11-12T12:47:45.519982319Z Command:  STOR /ftp/user/sortReport20251112124745.csv
2025-11-12T12:47:45.519996756Z Status:   Waiting for response to: STOR /ftp/user/sortReport20251112124745.csv
2025-11-12T12:47:45.521004890Z Response: 150 Ok to send data. [<1ms]
2025-11-12T12:47:45.524304429Z Status:   FTPS authentication successful, lib = .NET SslStream, cipher suite = Tls13 (Aes256, TLS_AES_256_GCM_SHA384, None, 0) [3ms]
2025-11-12T12:47:45.525583244Z Status:   Uploaded 181 bytes, <1ms, ?/s
2025-11-12T12:47:45.525597561Z Status:   Disposing(async) AsyncFtpClient.FtpSocketStream(data)
2025-11-12T12:47:45.526743250Z Status:   Waiting for response to: STOR /ftp/user/sortReport20251112124745.csv
2025-11-12T12:47:45.526758719Z Response: 426 Failure reading network stream. [6ms]

Questions

  1. Is this a known interoperability issue with certain FTP servers/NAT setups?
  2. Is there a recommended configuration (client or server) to avoid this (e.g., EPSV instead of PASV, disabling CCC, keep-alive/tcp settings, transfer chunking/flush behavior)?

Thanks in advance.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions