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);
                    }
                }
            }
Mort432
  • 86
  • 1
  • 9
  • 2
    The reader will throw this exception if it *is* disposed, not in the last line. Somehow, somewhere you are accessing a disposed object. Post the *full* exception, including its callstack. Perhaps the error isn't where you think it is. You can get the call stack with `Exception.ToString()` – Panagiotis Kanavos Feb 16 '18 at 14:40
  • Full exception added in edit :) – Mort432 Feb 16 '18 at 15:09
  • Which shows that what got disposed was the network stream, not the reader. Was there an error? Did you try reading inside an `async void` or some other kind of `fire-and-forget` method? The code that made the original call could have finished and *disposed* the response before the reader had a chance to finish – Panagiotis Kanavos Feb 16 '18 at 15:13
  • Post the code that *makes* the FtpWebRequest call. That `.d__12.MoveNext()` suggests that you used asynchronous code or an iterator. – Panagiotis Kanavos Feb 16 '18 at 15:18
  • I don't believe so. I'll add an edit that shows the rest of the code happening before it. All pretty basic I think. – Mort432 Feb 16 '18 at 15:18
  • Still, post it. The call stack shows that you *are* using constructs that could dispose of the response in advance. And your program *does* crash. If there was any issue with StreamReader or FtpWebResponse don't you think people would have noticed decades ago? – Panagiotis Kanavos Feb 16 '18 at 15:19
  • Sorry, I think I misspoke. It's possible something else is calling such a function, but I think I meant more "I don't recall writing anything like that, or see it in the areas I've touched today". This is an old codebase so there could be dragons anywhere :) – Mort432 Feb 16 '18 at 15:24
  • Not related to your current problem, but note that `FtpWebResponse` (and other responses, like `HttpWebResponse`) should also be disposed, and it's actually quite important to do that. – Evk Feb 16 '18 at 15:27
  • Show us [`FtpWebRequest` log file](https://stackoverflow.com/q/9664650/850848). – Martin Prikryl Feb 16 '18 at 15:34
  • Hey. Went through that. I'm not really able to share that as it contains client data, sorry. – Mort432 Feb 16 '18 at 15:41
  • @Mort432 after looking through the source of `FtpDataStream` it seems you are out of luck - it closes itself if there is no more data to download. What is the file's encoding? Is the StreamReader trying to read more data to decode a UTF8 character perhaps? You are downloading the file with `request.UseBinary = true;` which means the FTP server sends binary data, not text – Panagiotis Kanavos Feb 16 '18 at 15:49

3 Answers3

4

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.

Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
  • Thank you so much! Terrific answer. I've now refactored as per your suggestion and all is well. Thanks again. – Mort432 Feb 19 '18 at 09:23
  • @MartinPrikryl `m_NetworkStream.Read` fills a buffer array without caring about its contents. – Panagiotis Kanavos Feb 21 '18 at 09:23
  • 1
    OK. What I wanted to say: If you try to fix this by changing the condition to `!reader.EndOfStream`, you will still get this "disposed" exception (throws by `EndOfStream`), if the last line in the text file does to have EOL character(s). – Martin Prikryl Feb 21 '18 at 09:28
1

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);
        }
    }
}
Nullkiller
  • 91
  • 1
  • 4
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.

JuanR
  • 7,405
  • 1
  • 19
  • 30
  • @JuanR There's nothing wrong with disposing the Response stream once the reader is finished. On the contrary, one should *always* use `using` with streams. It's far more likely that `FtpWebResponse` was disposed by some other code before the reader had a chance to finish – Panagiotis Kanavos Feb 16 '18 at 15:14
  • @JuanR anyway, StreamReader itself will close the stream unless explicitly instructed to leave it open. Removing `using` from the stream won't have any effect – Panagiotis Kanavos Feb 16 '18 at 15:17
  • @PanagiotisKanavos: The stream is created by the `FtpWebResponse` object so it is its resposibility to dispose. You should always use `using` when you own the stream (e.g. when you instantiate it). – JuanR Feb 16 '18 at 15:29
  • @PanagiotisKanavos: Close != Dispose. – JuanR Feb 16 '18 at 15:29
  • @JuanR [yet it is](https://referencesource.microsoft.com/#mscorlib/system/io/stream.cs,252) – Panagiotis Kanavos Feb 16 '18 at 15:31
  • @JuanR documentation of this method (https://msdn.microsoft.com/en-us/library/system.net.webresponse.getresponsestream(v=vs.110).aspx) explicitly states that it should be closed "to avoid running out of system resources" – Evk Feb 16 '18 at 15:37
  • @Evk: Correct. Closed, not disposed. Read the sample in the very same link you posted. – JuanR Feb 16 '18 at 15:40
  • @JuanR closing and disposing stream is the same thing, because `Close()` just calls `Dispose()` in base `Stream` class (and I'm not aware of any specific stream that does that differently). – Evk Feb 16 '18 at 15:48
  • @Evk: From the documentation: The stream is automatically closed when you close the `FtpWebResponse` object that contains it. – JuanR Feb 16 '18 at 15:59
  • @JuanR the response is *not* closed or disposed. This is a case of .NET acting in an unexpected but understandable fashion. For streams in general, closing is identical to disposing, enforced by the base Stream class itself. In this case though, FtpDataStream will close, ie dispose itself if there are no more data to read. I suspect this happens in order to release *server* resources as soon as possible. FTP servers don't like long-lived open streams. – Panagiotis Kanavos Feb 20 '18 at 08:27