1

I'm trying to fetch a text file from an FTP server, then read it line by line, adding each line to a list.

My code seems to fit the standard pattern for this:

            var response = (FtpWebResponse)request.GetResponse();
            using (var responseStream = response.GetResponseStream())
            {
                using (var reader = new StreamReader(responseStream))
                {
                    string line;
                    while((line = reader.ReadLine()) != null)
                    {
                        lines.Add(line);
                    }
                }
            }

But, for some reason, when reader.ReadLine() is called on the very last line of the file, it throws an exception, saying "Cannot access a disposed object".

This is really weirding me out. If I remember correctly, the final line of a stream when there is no further data is null, right?

In addition (while I'm not certain about this), this only seems to be happening locally; the live version of this service seems to be pootling along fine (albeit with some issues I'm trying to get to the bottom of). I certainly don't see this issue in my logs.

Anyone have an ideas?

EDIT: Here's the full text of the exception.

System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'System.Net.Sockets.NetworkStream'.
   at System.Net.Sockets.NetworkStream.Read(Byte[] buffer, Int32 offset, Int32 size)
   at System.Net.FtpDataStream.Read(Byte[] buffer, Int32 offset, Int32 size)
   at System.IO.StreamReader.ReadBuffer()
   at System.IO.StreamReader.ReadLine()
   at CENSORED.CatalogueJobs.Common.FtpFileManager.ReadFile(String fileName, String directory) in C:\\Projects\\CENSORED_CatalogueJobs\\CENSORED.CatalogueJobs.Common\\FtpFileManager.cs:line 104
   at CENSORED.CatalogueJobs.CENSOREDDispatchService.CENSOREDDispatchesProcess.<Process>d__12.MoveNext() in C:\\Projects\\CENSORED_CatalogueJobs\\CENSORED.CatalogueJobs.CENSOREDDispatches\\CENSOREDDispatchesProcess.cs:line 95"

Type is "System.ObjectDisposedException". Sorry for censorship, exception contains my client's name.

EDIT 2: Here's the code now, after being expanded out and removing a layer of usings (I think I've done it right).

            var response = (FtpWebResponse)request.GetResponse();
            using (var reader = new StreamReader(response.GetResponseStream()))
            {
                string line = reader.ReadLine();
                while(line != null)
                {
                    lines.Add(line);
                    line = reader.ReadLine();
                }
            }

EDIT 3: A slightly more wide view of the code (temporarily reverted for my sanity). This is essentially everything in the function.

            var request = (FtpWebRequest)WebRequest.Create(_settings.Host + directory + fileName);

            request.Method = WebRequestMethods.Ftp.DownloadFile;
            request.Credentials = new NetworkCredential(_settings.UserName, _settings.Password);
            request.UsePassive = false;
            request.UseBinary = true;

            var response = (FtpWebResponse)request.GetResponse();
            using (var responseStream = response.GetResponseStream())
            {
                using (var reader = new StreamReader(responseStream))
                {
                    string line;
                    while((line = reader.ReadLine()) != null)
                    {
                        lines.Add(line);
                    }
                }
            }
4

3 回答 3

3

Checking the source of the internal FtpDataStream class shows that its Read method will close the stream all by itself if there are no more bytes:

    public override int Read(byte[] buffer, int offset, int size) {
        CheckError();
        int readBytes;
        try {
            readBytes = m_NetworkStream.Read(buffer, offset, size);
        } catch {
            CheckError();
            throw;
        }
        if (readBytes == 0)
        {
            m_IsFullyRead = true;
            Close();
        }
        return readBytes;
    }

Stream.Close() is a direct call to Dispose :

    public virtual void Close()
    {
        /* These are correct, but we'd have to fix PipeStream & NetworkStream very carefully.
        Contract.Ensures(CanRead == false);
        Contract.Ensures(CanWrite == false);
        Contract.Ensures(CanSeek == false);
        */

        Dispose(true);
        GC.SuppressFinalize(this);
    }

That's not how other streams, eg FileStream.Read work.

It looks like StreamReader.ReadLine is trying to read more data, resulting in an exception. This could be because it's trying to decode a UTF8 or UTF16 character at the end of the file.

Instead of reading line by line from the network stream, it would be better to copy it into a MemoryStream or FileStream with Stream.CopyTo before reading it

UPDATE

This behaviour, while totally unexpected, isn't unreasonable. The following is more of an educated guess based on the FTP protocol itself, the FtpWebRequest.cs and FtpDataStream.cs sources and painful experience trying to download multiple files.

FtpWebRequest is a (very) leaky abstraction on top of FTP. FTP is a connection oriented protocol with specific commands like LIST, GET and the missing MGET. That means, that once the server has finished sending data to the client in response to LIST or GET, it goes back to waiting for commands.

FtpWebRequest tries to hide this by making each request appear connectionless. This means that once the client finishes reading data from the Response stream there's no valid state to return to - an FtpWebResponse command can't be used to issue further commands. It can't be used to retrieve multiple files with MGET either, which is a major pain. There's only supposed to be one response stream after all.

With .NET Core and an increased need (to put it mildly) to use unsupported protocols like SFTP it may be a very good idea to find a better FTP client library.

于 2018-02-16T15:48:59.397 回答
0

I think what is being disposed is the Response stream.

You should not be putting a using statement around it. The stream resource belongs to the FtpWebResponse object.

Try removing that and see if the problem goes away.

You would also do yourself and others a service by expanding the code so you can properly step through it. It might reveal something else:

using (var reader = new StreamReader(responseStream))
{
    string line = reader.ReadLine();
    while(line != null)
    {
        lines.Add(line);
        line = reader.ReadLine();
    }
}

It's one more line of code and it makes it more readable and easier to debug.

于 2018-02-16T14:45:36.377 回答
0

I also encountered this problem. Looks like FtpDataStream resets CanRead and CanWrite to false after it is closed. So as a workaround you can check CanRead before reading next line to avoid ObjectDisposedException.

using (var responseStream = response.GetResponseStream())
{
    using (var reader = new StreamReader(responseStream))
    {
        string line;
        while(responseStream.CanRead && (line = reader.ReadLine()) != null)
        {
            lines.Add(line);
        }
    }
}
于 2019-05-08T12:59:39.410 回答