-
Notifications
You must be signed in to change notification settings - Fork 546
Try to avoid ObjectDisposedException
when writing output
#8632
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
It may happen that the contents stream in the `WriteRequestContentToOutput` method is disposed while asynchronously copying content to the connection output stream. This will then cause the `ObjectDisposedException` exception to be thrown when seeking back to the beginning of the stream. Try to avoid the problem by checking whether cancellation was requested during the call to `CopyToAsync` and, if we get past that point, catch the `ObjectDisposedException` to report it but not to rethrow it.
Context: https://discord.com/channels/732297728826277939/732297837953679412/1195284521378123836
|
public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
{
// This implementation offers better performance compared to the base class version.
ValidateCopyToArguments(destination, bufferSize);
EnsureNotClosed();
|
|
* main: Update a number of l18n files (#8633)
await stream.CopyToAsync(httpConnection.OutputStream!, 4096, cancellationToken).ConfigureAwait(false); | ||
} catch (ObjectDisposedException ex) { | ||
Logger.Log (LogLevel.Error, LOG_APP, $"Stream disposed while copying the content stream to output: {ex}"); | ||
cancellationToken.ThrowIfCancellationRequested (); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feels a tad odd: we're in a catch
block, therefore an exception was thrown and caught, but if (for some reason) cancellationToken.ThrowIfCancellationRequested()
doesn't throw, then…we continue execution? That doesn't feel right. Is potentially swallowing ObjectDisposedException
and doing nothing more really appropriate?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's fine, since the method returns a Task
and, thus, all the exceptions are observable - but it might not be the case if the thread is being cancelled.
await stream.CopyToAsync(httpConnection.OutputStream!, 4096, cancellationToken).ConfigureAwait(false); | ||
cancellationToken.ThrowIfCancellationRequested (); | ||
try { | ||
await stream.CopyToAsync(httpConnection.OutputStream!, 4096, cancellationToken).ConfigureAwait(false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given this observation, perhaps we should/also check httpConnection.OutputStream.CanWrite
.
If httpConnection.OutputStream.CanWrite
is false -- possibly because it was disposed, possibly because The Gods Must Be Crazy -- then What Should We Do? Throw an exception? Return a failed Task?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Checking CanWrite
only tells us whether the stream is capable of writing, not whether it's able to do so. IOW, in many cases in the BCL a true
returned from the property is not influenced by the stream state:
dotnet/runtime/src/libraries/System.Net.Http/src/System/Net/Http $ git grep -n 'bool CanWrite'
BrowserHttpHandler/BrowserHttpHandler.cs:391: public override bool CanWrite => true;
BrowserHttpHandler/BrowserHttpHandler.cs:542: public override bool CanWrite => false;
EmptyReadStream.cs:17: public override bool CanWrite => false;
HttpContent.cs:1053: public override bool CanWrite => true;
MultipartContent.cs:451: public override bool CanWrite => false;
SocketsHttpHandler/DecompressionHandler.cs:253: public override bool CanWrite => false;
SocketsHttpHandler/DecompressionHandler.cs:328: public override bool CanWrite => false;
SocketsHttpHandler/Http2Stream.cs:1438: public override bool CanWrite => false;
SocketsHttpHandler/Http2Stream.cs:1530: public override bool CanWrite => _http2Stream != null;
SocketsHttpHandler/Http3RequestStream.cs:1354: public override bool CanWrite => false;
SocketsHttpHandler/Http3RequestStream.cs:1446: public override bool CanWrite => _stream != null;
SocketsHttpHandler/HttpContentReadStream.cs:21: public sealed override bool CanWrite => false;
SocketsHttpHandler/HttpContentWriteStream.cs:21: public sealed override bool CanWrite => _connection != null;
SocketsHttpHandler/RawConnectionStream.cs:22: public sealed override bool CanWrite => _connection != null;
StreamContent.cs:153: public override bool CanWrite => false;
While the content streams do change the value of the property based on (portions) of their state, I don't think we should (and can) rely on that value, as per docs:
When overridden in a derived class, gets a value indicating whether the current stream supports writing.
I read the "supports" as "is capable of" and not "is able to right now".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think "the current stream" is the important clause. Consider code such as:
if (stream.CanWrite) {
stream.Write(…);
}
if that stream.Write()
will throw, then what's the point of the stream.CanWrite
check? (Sure, it can't check everything, e.g. the network might suddenly disappear or your disk runs out of space, so stream.Write(…)
may throw. However, if you know that all writes will fail, then stream.CanWrite
should return false!)
* main: (99 commits) LEGO: Merge pull request 8818 Bump to dotnet/installer@b40c44502d 9.0.100-preview.3.24165.20 (#8817) Bump com.android.tools:r8 from 8.2.47 to 8.3.37 (#8816) [Mono.Android] Prevent NullPointerException in TranslateStackTrace (#8795) Localized file check-in by OneLocBuild Task (#8815) [Xamarin.Android.Build.Tasks] Make all assemblies RID-specific (#8478) Localized file check-in by OneLocBuild Task (#8813) [Xamarin.Android.Build.Tasks] %(AndroidAsset.AssetPack) Support (#8631) [runtime] Remove the last vestiges of desktop builds (#8810) [ci] Don't auto-retry APK test suites. (#8811) [Microsoft.Android.Templates] Update EN l10n template strings (#8808) Bump to xamarin/Java.Interop/main@651de42 (#8809) [Mono.Android] is now "trimming safe" (#8778) [Mono.Android] Fix missing enum issues that cause BG8800 warnings. (#8707) Bump external/Java.Interop from `3436a30` to `5bca8ad` (#8803) Bump to xamarin/monodroid@77124dc1 (#8804) Bump to dotnet/installer@e911f5c82c 9.0.100-preview.3.24161.2 (#8802) Bump to xamarin/Java.Interop/main@3436a30 (#8799) [templates] Remove redundant "template" from display name. (#8773) Bump to xamarin/Java.Interop/main@a7e09b7 (#8793) ...
I think this PR existed in order to attempt to address dotnet/runtime#115611. dotnet/runtime#115611 increasingly looks like a GC bug; see also: A workaround for the issue has also been posted: Given the above, do we still want this PR? |
It may happen that the contents stream in the
WriteRequestContentToOutput
method is disposed while asynchronouslycopying content to the connection output stream. This will then cause
the
ObjectDisposedException
exception to be thrown when seeking backto the beginning of the stream.
Try to avoid the problem by checking whether cancellation was requested
during the call to
CopyToAsync
and, if we get past that point, catchthe
ObjectDisposedException
to report it but not to rethrow it.