Skip to content
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

Try to avoid ObjectDisposedException when writing output #8632

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

grendello
Copy link
Member

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.

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.
@jonpryor
Copy link
Member

Context: https://discord.com/channels/732297728826277939/732297837953679412/1195284521378123836

…i keep getting.

System.IO.MemoryStream.CopyToAsync(Stream , Int32 , CancellationToken )
System.ObjectDisposedException: ObjectDisposed_StreamClosed

[the following call stack is sent to] my appcenter from users
with stacktrace.

System.IO.MemoryStream.CopyToAsync(Stream , Int32 , CancellationToken )
Xamarin.Android.Net.AndroidMessageHandler.WriteRequestContentToOutput(HttpRequestMessage , HttpURLConnection , CancellationToken )
Xamarin.Android.Net.AndroidMessageHandler.DoProcessRequest(HttpRequestMessage , URL , HttpURLConnection , CancellationToken , RequestRedirectionState )
Xamarin.Android.Net.AndroidMessageHandler.DoSendAsync(HttpRequestMessage , CancellationToken )
System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage , HttpCompletionOption , CancellationTokenSource , Boolean , CancellationTokenSource , CancellationToken )
MayApp.RestManager.ExecuteRest(String RequestType, StringContent Jsonmodel, Nullable`1 retry)

@jonpryor
Copy link
Member

Context: https://github.com/dotnet/runtime/blob/release/8.0/src/libraries/System.Private.CoreLib/src/System/IO/MemoryStream.cs#L471

        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();

EnsureNotClosed() throws ObjectDissposedException() if the stream isn't open.

@jonpryor
Copy link
Member

Context: https://github.com/dotnet/runtime/blob/release/8.0/src/libraries/System.Private.CoreLib/src/System/IO/Stream.cs#L996-L1011

Stream.ValidateCopyToArguments(*stream, int) throws ObjectDisposedException with ObjectDisposed_StreamClosed if destination is closed, i.e. if httpConnection.OutputStream! has been disposed, ObjectDisposedException can also be thrown from MemoryStream.CopyToAsync()!

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 ();
Copy link
Member

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?

Copy link
Member Author

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);
Copy link
Member

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?

Copy link
Member Author

@grendello grendello Feb 16, 2024

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".

* 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@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@3436a30 (#8799)
  [templates] Remove redundant "template" from display name. (#8773)
  Bump to xamarin/java.interop@a7e09b7 (#8793)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants