Summary
I am trying to implement a basic retry handler in Xamarin Android and Xamarin iOS which on receipt of a 401 response refreshes an access token and then retries the request with the new access token, however, both platforms result in errors when using the native Message handlers.
I am using a DelegatingHandler to bypass the restriction on HttpClient for the reuse of the HttpRequest message but the error is deeper than that.
The AndroidClientHandler class and the newer AndroidMessageHandler read the Content as a stream then dispose of it as its ReadAsStreamAsync inside a using context.
The iOS NSUrlSessionHandler does not rewind the stream resulting in the retry not containing the correct content.
Additionally, this comment on the dotnet runtime github suggests that reusing HttpRequestMessages is unsupported behaviour: Link
This error does not occur with the managed HttpClient option however, particularly on Android we need the AndroidClientHandler in order to get proper SSL and Https support, login throws exceptions without it.
I was able to hack around it by creating a CustomAndroidMessageHandler and removing the using statement on Android as the AndroidMessageHandler is extensible however this is not possible on iOS.
The question is whether I am using it correctly and if this is possible with the Xamarin Handlers.
Am I missing the trick here?
Expected behavior:
Successfully retries the request with the correct content.
Actual behaviour:
Android: Cannot access a closed stream
iOS: Empty request content, missing content causes request to fail.
Answers
Polly - 'Cannot access a closed Stream'
The outcome of this quest seems to be the you CANNOT do this, however the Dotnet library clearly does exactly this with their Polly retry implementation here so that does not seem to be correct.
Is this purely a case of Xamarin not supporting this?
Code Snippets
Register platform specific HttpMessageHandler using MvvmCross IoC container
Mvx.IoCProvider.RegisterSingleton<HttpMessageHandler>(() => new AndroidMessageHandler());
Method to register HttpClient to Service Collection with Polly and AuthenticationDelegatingHandler
var builder = services.AddHttpClient<TClient, TImplementation>()
.ConfigurePrimaryHttpMessageHandler(() => Mvx.IoCProvider.Resolve<HttpMessageHandler>());
if (requiresAuthorization)
{
builder.AddHttpMessageHandler<AuthenticationDelegatingHandler>();
}
DelegatingHandler Send Async
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _sessionService.AccessToken);
var response = await base.SendAsync(request, cancellationToken);
if (response.StatusCode is not (HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden))
{
return response;
}
var refreshToken = _sessionService.RefreshToken;
var tokenResponse = await _authenticationClient.Refresh(refreshToken);
if (!tokenResponse.IsError)
{
await _sessionService.UpdateTokens(tokenResponse.AccessToken, tokenResponse.RefreshToken);
try
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponse.AccessToken);
return await base.SendAsync(request, cancellationToken);
}
catch (Exception e)
{
_logger.LogError(e, "Post token refresh request retry failed");
return response;
}
}
await _sessionService.EndSessionAndLogout();
return response;
}
Custom Android Client Handler Hack
public class CustomAndroidMessageHandler : AndroidMessageHandler
{
protected override async Task WriteRequestContentToOutput(
HttpRequestMessage request,
HttpURLConnection httpConnection,
CancellationToken cancellationToken)
{
var stream = await request.Content.ReadAsStreamAsync().ConfigureAwait(false);
await stream.CopyToAsync(httpConnection.OutputStream!, 4096, cancellationToken).ConfigureAwait(false);
if (stream.CanSeek)
{
stream.Seek(0, SeekOrigin.Begin);
}
}
}
Open Related Github Issues: