You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
StreamReader.Peek() on a DeflateStream returns -1 early #68983
I have code that has a DeflateStream and uses a StreamReader on it - this code works fine in .NET Framework, .NET Core 3.1, and .NET 5 - but it breaks in .NET 6.
When trying to Peek() on the stream, internally StreamReader reads a block out that ends up being smaller than it requested, I'm pretty sure this is due to this breaking change. After that happens, it sets a flag called "_isBlocked" and starts returning -1 from Peek() which is not expected.
This seems like a bug in StreamReader?
Reproduction Steps
Construct a StreamReader over a DeflateStream
Read for a while, peeking as you go
Expected behavior
Peek() returns -1 only at the end of the stream, as it did before
Actual behavior
Peek() returns -1 early in the middle of the file.
Regression?
Yes, regressed in .NET 6
Known Workarounds
Can use StreamReader.EndOfStream instead of looking for -1 from Peek(), but this was still a difficult to hunt down bug on upgrading the .NET version of this library.
@stephentoub, I have made a really small repro. Take this code:
usingSystem;usingSystem.IO;usingSystem.IO.Compression;usingSystem.Net;usingSystem.Runtime.InteropServices;Console.WriteLine($"Running in {RuntimeInformation.FrameworkDescription}");varlocalPath=Path.GetTempFileName();newWebClient().DownloadFile(@"https://github.com/dotnet/runtime/archive/refs/tags/v6.0.5.zip",localPath);varnumChars=0;usingvararchive=newZipArchive(File.OpenRead(localPath),ZipArchiveMode.Read);foreach(varentryinarchive.Entries){usingvarsr=newStreamReader(entry.Open());while(sr.Peek()!=-1){charnext=(char)sr.Read();numChars++;}}Console.WriteLine($"Read {numChars} characters in {archive.Entries.Count} zip entries");
When run on .NET 5 vs. 6, I get different numbers of characters read because Peek() returns -1 "early" sometimes in .NET 6, probably due to the breaking change I mentioned in the original post in this issue. I get this output:
Running in .NET 5.0.17
Read 878322628 characters in 69097 zip entries
vs.
Running in .NET 6.0.5
Read 645700952 characters in 69097 zip entries
usingSystem;usingSystem.IO;usingvarstream=newAStream();usingvarreader=newStreamReader(stream);// prints "42" oncewhile(reader.Peek()!=-1){Console.WriteLine(reader.Read());}classAStream:Stream{publicoverrideintRead(byte[]buffer,intoffset,intcount){buffer[offset]=42;return1;}publicoverrideboolCanRead=>true;publicoverrideboolCanSeek=>true;publicoverrideboolCanWrite=>thrownewNotImplementedException();publicoverridelongLength=>thrownewNotImplementedException();publicoverridelongPosition{get=>thrownewNotImplementedException();set=>thrownewNotImplementedException();}publicoverridevoidFlush()=>thrownewNotImplementedException();publicoverridelongSeek(longoffset,SeekOriginorigin)=>thrownewNotImplementedException();publicoverridevoidSetLength(longvalue)=>thrownewNotImplementedException();publicoverridevoidWrite(byte[]buffer,intoffset,intcount)=>thrownewNotImplementedException();}
Note that the documentation of Peek leaves me confused. It says that -1 can also be returned if the stream does not support seeking, but that would make Peek much less useful and it's not what the implementation does.
StreamReader has a private _isBlocked field that it's using to track whether the last read on the underlying Stream returned fewer bytes than requested:
// Whether the stream is most likely not going to give us back as much
// data as we want the next time we call it. We must do the computation
// before we do any byte order mark handling and save the result. Note
// that we need this to allow users to handle streams used for an
// interactive protocol, where they block waiting for the remote end
// to send a response, like logging in on a Unix machine.
privatebool_isBlocked;
It then uses this field in various ReadXx methods to determine whether to try to read more data from the underlying Stream or to just give back to the caller just the non-zero amount it gathered thus far. I think that's a misguided design but at least defensible. However, it's then also using that _isBlocked in Peek():
Activity
stephentoub commentedon May 6, 2022
Can you please share a small, isolated repro?
aromaa commentedon May 6, 2022
Sounds like this could be #61325 (comment).
Austin-Lamb commentedon May 9, 2022
This comes from a large project, I will see if I can extract a minimal repro - it may take me a couple days.
Austin-Lamb commentedon May 10, 2022
@stephentoub, I have made a really small repro. Take this code:
When run on .NET 5 vs. 6, I get different numbers of characters read because Peek() returns -1 "early" sometimes in .NET 6, probably due to the breaking change I mentioned in the original post in this issue. I get this output:
vs.
svick commentedon May 11, 2022
Here is a self-contained repro of this behavior:
Note that the documentation of
Peekleaves me confused. It says that -1 can also be returned if the stream does not support seeking, but that would makePeekmuch less useful and it's not what the implementation does.stephentoub commentedon May 11, 2022
Thanks for the repros. I'll take a look...
stephentoub commentedon May 11, 2022
StreamReader has a private _isBlocked field that it's using to track whether the last read on the underlying Stream returned fewer bytes than requested:
runtime/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs
Lines 58 to 64 in 336a5d2
It then uses this field in various ReadXx methods to determine whether to try to read more data from the underlying Stream or to just give back to the caller just the non-zero amount it gathered thus far. I think that's a misguided design but at least defensible. However, it's then also using that _isBlocked in Peek():
runtime/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs
Line 322 in 336a5d2
which while potentially "by design" is to me a flawed design.
I think the fix here at a minimum is to change the semantics of Peek to stop looking at _isBlocked at all.
@adamsitnik?
mpervovskiy commentedon May 23, 2022
This becomes even worse with Http streams as
Peek()is unpredictable there... and looks like this issue dwells inStreamReaderfor ages.Repo:
Differences are happening at different places in the stream
Replaced StreamReader.Peek() (#1380)