Surface HttpRequestException from AndroidMessageHandler#11682
Conversation
…andler Co-authored-by: jonathanpeppers <840039+jonathanpeppers@users.noreply.github.com>
Co-authored-by: jonathanpeppers <840039+jonathanpeppers@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR updates Xamarin.Android.Net.AndroidMessageHandler to align with the HttpClient.SendAsync contract by surfacing request-path transport/protocol failures as HttpRequestException (while preserving the legacy WebException + WebExceptionStatus via InnerException for back-compat).
Changes:
- Replace
WebExceptionthrow sites in the request path withHttpRequestException, preserving aWebException(with status) as the inner exception. - Centralize exception creation in a
CreateHttpRequestException(...)helper. - Add tests asserting
HttpRequestExceptionis thrown for connection failures and exceedingMaxAutomaticRedirections.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs |
Switches request-path failures to HttpRequestException and introduces a helper to preserve legacy WebException details. |
tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidMessageHandlerTests.cs |
Adds regression tests to validate the new exception surface and inner-exception preservation expectations. |
| @@ -839,7 +846,7 @@ protected virtual async Task WriteRequestContentToOutput (HttpRequestMessage req | |||
| 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); | |||
| throw CreateHttpRequestException (ex.Message, ex, WebExceptionStatus.ConnectFailure); | |||
|
@copilot address the code review comments left above |
Co-authored-by: jonathanpeppers <840039+jonathanpeppers@users.noreply.github.com>
Addressed all three review comments in 28f733d:
|
|
/review |
|
✅ Android PR Reviewer completed successfully! |
There was a problem hiding this comment.
✅ LGTM
A focused, well-reasoned change. AndroidMessageHandler now surfaces transport/protocol failures as HttpRequestException (per the HttpClient.SendAsync contract) while preserving the legacy WebException — including its WebExceptionStatus — as the InnerException for code migrated from classic Xamarin.Android. I formed an independent assessment of the diff and then reconciled it with the PR description; the claims hold up.
Verified:
- All
WebExceptionthrow sites converted. Every throw site in the request path now routes through theCreateHttpRequestExceptionhelper; no bareWebExceptionthrows remain. ✓ - The previously-dead
catch (Java.Net.ConnectException)is now reachable.ConnectAsyncwraps every connection failure inHttpRequestException, so the old catch could never match. The newcatch (HttpRequestException ex) when (ex.InnerException is Java.Net.ConnectException)correctly unwraps and re-wraps it so theWebException(ConnectFailure)back-compat path actually runs. ✓ - Status preservation is correct.
WebExceptionStatus.UnknownErrorfor the max-redirections / invalid-redirect-URI paths matches what the oldWebException(string)constructor produced, so status-inspecting code is unaffected. ✓ - No collateral breakage. No other product code or test asserts a bare
WebExceptionfrom this handler;AssertRejectsRemoteCertificatealready catches both types and keeps passing. ✓ - Regression tests added for the connect-failure and max-redirections paths, asserting the inner
WebException.Status(tracedMaxAutomaticRedirections = 1→ throws on the first redirect, so the single-BeginGetContextlistener is sufficient). ✓ - CI is green — 40/40 checks, including the macOS emulator device-test legs that exercise
Mono.Android-Tests. ✓
Severity counts: ❌ 0 ·
- Populate
HttpRequestException.HttpRequestErrorfor fuller modern-contract conformance. - The inner-
WebExceptionshim only coversJava.Net.ConnectException; DNS/timeout connect failures aren't classified (pre-existing gap, not a regression).
Minor follow-up (pre-existing, not inlineable): the comment at AndroidMessageHandlerTests.cs:308-310 ("...we throw both of these...") and the now-unreachable catch (System.Net.WebException) {} on line 311 are stale after this change — the handler no longer throws a bare WebException. Worth tidying in a follow-up (stale-comment / dead-code cleanup).
Nice work — no blocking issues.
Generated by Android PR Reviewer for issue #11682 · 793.2 AIC · ⌖ 47.2 AIC · ⊞ 37.8K
Comment /review to run again
| // 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)); |
There was a problem hiding this comment.
🤖 💡 API design — Optional/non-blocking: since Mono.Android targets modern .NET, consider also populating HttpRequestException.HttpRequestError (added in .NET 8) here, in addition to the inner WebException. The point of this change is to honor the HttpClient.SendAsync contract, and SocketsHttpHandler sets values like HttpRequestError.ConnectionError / SecureConnectionError so callers can branch on ex.HttpRequestError rather than unwrapping the legacy WebException. e.g. for the connect-failure path:
new HttpRequestException (HttpRequestError.ConnectionError, message, new WebException (message, innerException, status, null))Mapping each WebExceptionStatus to an HttpRequestError is a bit more than a one-liner, so feel free to defer — just flagging it for fuller modern-contract conformance.
Rule: Conform to the modern HttpClient contract
There was a problem hiding this comment.
Deferring to a follow-up. Mapping each WebExceptionStatus to an HttpRequestError (e.g. ConnectFailure → ConnectionError, SecureChannelFailure → SecureConnectionError, Timeout → ?, ProtocolError → ?, UnknownError → ?) needs deliberation and its own test coverage, and would expand the scope of this otherwise-focused back-compat PR. Filing a follow-up issue.
| 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) { |
There was a problem hiding this comment.
🤖 💡 Patterns — Optional/non-blocking: this back-compat shim restores an inner WebException only for Java.Net.ConnectException. Other failures that ConnectAsync wraps the same way — DNS (Java.Net.UnknownHostException → NameResolutionFailure) and connect timeouts (Java.Net.SocketTimeoutException → Timeout) — still surface as HttpRequestException whose InnerException is the raw Java exception, so WebException-walking code (including the IsConnectionFailure test helper) won't classify them. This is a pre-existing gap, not a regression — before this change the old catch (Java.Net.ConnectException) was dead, so no connect failure got the WebException wrapper; this PR strictly improves the most common case. A follow-up could map the DNS/timeout cases too for completeness.
Rule: Preserve back-compat consistently
There was a problem hiding this comment.
Agreed — this is a pre-existing gap (the old catch (Java.Net.ConnectException) was dead, so DNS/timeout never got the WebException wrapper either). Out of scope for this PR; tracking in a follow-up issue alongside the HttpRequestError work.
After this PR, AndroidMessageHandler no longer throws bare WebException -- all throw sites route through CreateHttpRequestException, which wraps in HttpRequestException (with WebException as InnerException). The catch (System.Net.WebException) {} in AssertRejectsRemoteCertificate is therefore unreachable, and the comment claiming we throw both is stale.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Thanks for the review! Addressed the stale-comment / dead-code follow-up in 56138fd: removed the now-unreachable Deferring the two inline 💡 suggestions ( |
Xamarin.Android.Net.AndroidMessageHandlerreported transport/protocol failures as the legacySystem.Net.WebException, contradicting the documentedHttpClient.SendAsynccontract (which throwsHttpRequestException).Changes
AndroidMessageHandler: allWebExceptionthrow sites in the request path (timeout, protocol, security, IO, connect, max‑redirections, invalid redirect URI, protocol‑method setup) now throwHttpRequestException.WebException— including itsWebExceptionStatus— is kept as theInnerException, so code migrated from classic Xamarin.Android that inspects status (and existing test helpers that walk the inner chain) keeps working. Centralized in one helper:ConnectAsyncwraps every connection failure inHttpRequestException, soDoProcessRequestnow catches thatHttpRequestExceptionwhen itsInnerExceptionis aJava.Net.ConnectException, unwraps it, and re-wraps it viaCreateHttpRequestExceptionwithWebExceptionStatus.ConnectFailure. Previously thecatch (Java.Net.ConnectException)block was unreachable, so connection failures did not get the intended innerWebException(ConnectFailure)back-compat wrapping.HttpRequestExceptionwith a preserved innerWebExceptionwhoseStatus == ConnectFailure; exceedingMaxAutomaticRedirectionsassertsHttpRequestExceptionwith a preserved innerWebExceptionwhoseStatus == UnknownError.Notes
WebExceptionat the top level will no longer match — this is the intended behavioral change. TheWebExceptionremains reachable viaex.InnerException.