Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Net.Http;

using Android.Runtime;
using Java.Net;

namespace Xamarin.Android.Net
Expand Down Expand Up @@ -39,14 +41,31 @@ public AndroidHttpResponseMessage (URL javaUrl, HttpURLConnection httpConnection

protected override void Dispose (bool disposing)
{
base.Dispose(disposing);
// Dispose the content first (base.Dispose). For a streaming response the content is a
// CancellationAwareResponseStream, which safely aborts any in-flight read and closes the
// underlying stream + connection itself (see AndroidMessageHandler.CancellationAwareResponseStream).
base.Dispose (disposing);

if (javaUrl != null) {
javaUrl.Dispose ();
}

if (httpConnection != null) {
httpConnection.Dispose ();
// Release the connection with Disconnect(), never Dispose(), on the Java peer:
// * Disconnect() closes the socket and aborts any in-flight read. It is the backstop for
// non-streaming responses (e.g. the buffered StringContent error paths) whose content does
// not own the connection; for a streaming response the content already disconnected via
// base.Dispose above, and Disconnect() is idempotent.
// * Disposing the peer (deleting its JNI global reference) could race a body read still
// unwinding on another thread and crash; the peer is reclaimed on finalization.
// This runs synchronously on the disposing thread (which may be the UI thread, e.g. gRPC
// cancelling from a UI callback): closing a socket is fast and, unlike connect/read/write,
// does not trigger NetworkOnMainThreadException.
try {
httpConnection.Disconnect ();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 ⚠️ Documentation — Two rationales in this PR conflict about running HttpURLConnection.Disconnect() on the (possibly UI) disposing thread. Here the comment says the synchronous Disconnect() is safe on the UI thread because "closing a socket ... does not trigger NetworkOnMainThreadException", yet CancellationAwareResponseStream.RequestDisconnect goes out of its way to background the very same call because "Disconnect() performs socket I/O that must not run there." Both can't be true — please reconcile.

Relatedly, the comment on the lines above ("for a streaming response the content already disconnected via base.Dispose above") is inaccurate for the in-flight case: when a body read is parked, base.Dispose only schedules a backgrounded disconnect and defers the close, so this synchronous Disconnect() is frequently the call that actually aborts the parked read — on the disposing thread. Worth clarifying so the "Dispose() never blocks" invariant in the class docs isn't misleading.

(Verify: same httpConnection peer is shared by AndroidHttpResponseMessage and CancellationAwareResponseStream.)

} catch (Exception ex) {
Logger.Log (LogLevel.Info, AndroidMessageHandler.LOG_APP, $"Disconnection exception: {ex}");
}
}
}
}
Expand Down
Loading