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
29 changes: 19 additions & 10 deletions src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -710,13 +710,13 @@ string EncodeUrl (Uri url)
request.Method = redirectState.Method;
request.RequestUri = redirectState.NewUrl;
} catch (Java.Net.SocketTimeoutException ex) when (JNIEnv.ShouldWrapJavaException (ex)) {
throw new WebException (ex.Message, ex, WebExceptionStatus.Timeout, null);
throw CreateHttpRequestException (ex.Message, ex, WebExceptionStatus.Timeout);
} catch (Java.Net.UnknownServiceException ex) when (JNIEnv.ShouldWrapJavaException (ex)) {
throw new WebException (ex.Message, ex, WebExceptionStatus.ProtocolError, null);
throw CreateHttpRequestException (ex.Message, ex, WebExceptionStatus.ProtocolError);
} catch (Java.Lang.SecurityException ex) when (JNIEnv.ShouldWrapJavaException (ex)) {
throw new WebException (ex.Message, ex, WebExceptionStatus.SecureChannelFailure, null);
throw CreateHttpRequestException (ex.Message, ex, WebExceptionStatus.SecureChannelFailure);
} catch (Java.IO.IOException ex) when (JNIEnv.ShouldWrapJavaException (ex)) {
throw new WebException (ex.Message, ex, WebExceptionStatus.UnknownError, null);
throw CreateHttpRequestException (ex.Message, ex, WebExceptionStatus.UnknownError);
}
}
}
Expand Down Expand Up @@ -780,6 +780,13 @@ Task ConnectAsync (HttpURLConnection httpConnection, CancellationToken ct)
}, ct);
}

// As documented for HttpClient.SendAsync (see https://github.com/dotnet/android/issues/5761), transport
// failures must be surfaced as HttpRequestException rather than the legacy WebException. To avoid breaking
// existing code that migrated from classic Xamarin.Android and still inspects WebException (and its
// WebExceptionStatus), we keep the original WebException as the inner exception.
static HttpRequestException CreateHttpRequestException (string message, Exception? innerException, WebExceptionStatus status)
=> new HttpRequestException (message, new WebException (message, innerException, status, null));

protected virtual async Task WriteRequestContentToOutput (HttpRequestMessage request, HttpURLConnection httpConnection, CancellationToken cancellationToken)
{
if (request.Content is null)
Expand Down Expand Up @@ -835,11 +842,13 @@ protected virtual async Task WriteRequestContentToOutput (HttpRequestMessage req
await ConnectAsync (httpConnection, cancellationToken).ConfigureAwait (continueOnCapturedContext: false);
if (Logger.LogNet)
Logger.Log (LogLevel.Info, LOG_APP, $" connected");
} catch (Java.Net.ConnectException ex) {
} catch (HttpRequestException ex) when (ex.InnerException is Java.Net.ConnectException connectException) {
if (Logger.LogNet)
Logger.Log (LogLevel.Info, LOG_APP, $"Connection exception {ex}");
// Wrap it nicely in a "standard" exception so that it's compatible with HttpClientHandler
throw new WebException (ex.Message, ex, WebExceptionStatus.ConnectFailure, null);
// `ConnectAsync` wraps all connection failures in HttpRequestException, so we unwrap the original
// `Java.Net.ConnectException` here and re-wrap it to preserve the legacy WebException (with its
// WebExceptionStatus) as the inner exception for back-compat with classic Xamarin.Android.
throw CreateHttpRequestException (connectException.Message, connectException, WebExceptionStatus.ConnectFailure);
}

if (cancellationToken.IsCancellationRequested) {
Expand Down Expand Up @@ -1086,7 +1095,7 @@ bool HandleRedirect (HttpStatusCode redirectCode, HttpURLConnection httpConnecti

redirectState.RedirectCounter++;
if (redirectState.RedirectCounter >= MaxAutomaticRedirections)
throw new WebException ($"Maximum automatic redirections exceeded (allowed {MaxAutomaticRedirections}, redirected {redirectState.RedirectCounter} times)");
throw CreateHttpRequestException ($"Maximum automatic redirections exceeded (allowed {MaxAutomaticRedirections}, redirected {redirectState.RedirectCounter} times)", null, WebExceptionStatus.UnknownError);

Uri redirectUrl;
try {
Expand Down Expand Up @@ -1123,7 +1132,7 @@ bool HandleRedirect (HttpStatusCode redirectCode, HttpURLConnection httpConnecti
if (Logger.LogNet)
Logger.Log (LogLevel.Debug, LOG_APP, $"Cooked redirect location: {redirectUrl}");
} catch (Exception ex) {
throw new WebException ($"Invalid redirect URI received: {location}", ex);
throw CreateHttpRequestException ($"Invalid redirect URI received: {location}", ex, WebExceptionStatus.UnknownError);
}

UriBuilder? builder = null;
Expand Down Expand Up @@ -1308,7 +1317,7 @@ protected virtual Task SetupRequest (HttpRequestMessage request, HttpURLConnecti
try {
httpConnection.RequestMethod = request.Method.ToString ();
} catch (Java.Net.ProtocolException ex) when (JNIEnv.ShouldWrapJavaException (ex)) {
throw new WebException (ex.Message, ex, WebExceptionStatus.ProtocolError, null);
throw CreateHttpRequestException (ex.Message, ex, WebExceptionStatus.ProtocolError);
}

// SSL context must be set up as soon as possible, before adding any content or
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -423,5 +423,51 @@ public async Task HttpContentStreamIsRewoundAfterCancellation ()

listener.Close ();
}

[Test]
public void ConnectionFailureThrowsHttpRequestException ()
{
// https://github.com/dotnet/android/issues/5761
// HttpClient.SendAsync is documented to throw HttpRequestException when there is a problem
// connecting to the server. It must not surface the legacy WebException as the primary exception.
int unusedPort = GetAvailablePort ();
using var client = new HttpClient (new AndroidMessageHandler ());

var ex = Assert.CatchAsync (async () => await client.GetAsync ($"http://localhost:{unusedPort}/"));
Assert.IsInstanceOf<HttpRequestException> (ex, $"Expected HttpRequestException but got {ex?.GetType ()}: {ex?.Message}");
var inner = ex?.InnerException as WebException;
Assert.IsNotNull (inner, $"Expected inner WebException but got {ex?.InnerException?.GetType ()}");
Assert.AreEqual (WebExceptionStatus.ConnectFailure, inner.Status, "Inner WebException should preserve ConnectFailure status");
}

[Test]
public void ExceedingMaxAutomaticRedirectionsThrowsHttpRequestException ()
{
// https://github.com/dotnet/android/issues/5761
// Failures in the request path must be surfaced as HttpRequestException (per the HttpClient.SendAsync
// contract). For back-compat with code migrated from classic Xamarin.Android, the legacy WebException
// (and its WebExceptionStatus) is preserved as the inner exception.
int port = GetAvailablePort ();
using var listener = new HttpListener ();
listener.Prefixes.Add ($"http://+:{port}/");
listener.Start ();
listener.BeginGetContext (ar => {
var ctx = listener.EndGetContext (ar);
ctx.Response.StatusCode = 302;
ctx.Response.RedirectLocation = $"http://localhost:{port}/";
ctx.Response.Close ();
}, null);

var handler = new AndroidMessageHandler { MaxAutomaticRedirections = 1 };
using var client = new HttpClient (handler);

var ex = Assert.CatchAsync (async () => await client.GetAsync ($"http://localhost:{port}/"));
listener.Close ();

Assert.IsInstanceOf<HttpRequestException> (ex, $"Expected HttpRequestException but got {ex?.GetType ()}: {ex?.Message}");
var inner = ex?.InnerException as WebException;
Assert.IsNotNull (inner, $"Expected inner WebException but got {ex?.InnerException?.GetType ()}");
Assert.AreEqual (WebExceptionStatus.UnknownError, inner.Status, "Inner WebException should preserve UnknownError status");
}
}
}