Xamarin原生Message Handler不支持HttpRequestMessage复用的重试问题
Xamarin双平台401重试刷新Token的问题与解决方案
问题背景
在Xamarin Android和Xamarin iOS中实现401/403自动重试逻辑:收到未授权响应时刷新AccessToken,再重试原请求,但使用原生HttpMessageHandler时遇到平台特定问题:
- Android:
AndroidClientHandler/AndroidMessageHandler会在using上下文内读取并释放请求Content流,重试时抛出「无法访问已关闭的流」错误; - iOS:
NSUrlSessionHandler不会重置流指针,导致重试时请求内容为空,请求失败。
额外背景:
- .NET Runtime官方明确复用
HttpRequestMessage属于未支持行为,但Polly重试库却能实现重试; - 使用托管
HttpClient无此问题,但Android端必须依赖原生Handler才能获得正确的SSL/HTTPS支持,否则登录会抛出异常; - 已通过自定义
CustomAndroidMessageHandler移除using语句临时解决Android端问题,但iOS端无法用同样方式处理; - Xamarin双平台均存在对应的平台特定Handler流处理问题的官方记录。
代码片段
1. 注册平台特定HttpMessageHandler
Mvx.IoCProvider.RegisterSingleton<HttpMessageHandler>(() => new AndroidMessageHandler());
2. 注册HttpClient并配置Polly与认证委托处理器
var builder = services.AddHttpClient<TClient, TImplementation>() .ConfigurePrimaryHttpMessageHandler(() => Mvx.IoCProvider.Resolve<HttpMessageHandler>()); if (requiresAuthorization) { builder.AddHttpMessageHandler<AuthenticationDelegatingHandler>(); }
3. 认证委托处理器的SendAsync方法
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; }
4. Android端临时解决方案:自定义MessageHandler
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); } } }
正确实现方案
核心思路:避免复用HttpRequestMessage
由于.NET官方不支持复用HttpRequestMessage,原生Handler的流处理问题本质是请求内容只能被消费一次。正确做法是在重试前重新生成请求副本,确保每次请求都有可重复读取的内容流。
步骤1:实现可重复读取的请求内容
对于带Content的请求(如POST/PUT),将Content转换为字节数组缓存,自定义可重复读取的HttpContent:
public class RepeatableContent : ByteArrayContent { public RepeatableContent(byte[] content) : base(content) { } protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context) { await base.SerializeToStreamAsync(stream, context); if (stream.CanSeek) { stream.Seek(0, SeekOrigin.Begin); } } }
步骤2:在重试前克隆请求
修改AuthenticationDelegatingHandler的SendAsync方法,重试时创建原请求的深拷贝:
private async Task<HttpRequestMessage> CloneRequest(HttpRequestMessage original) { var clone = new HttpRequestMessage(original.Method, original.RequestUri) { Version = original.Version, Method = original.Method }; // 复制请求头 foreach (var header in original.Headers) { clone.Headers.TryAddWithoutValidation(header.Key, header.Value); } // 复制请求内容(如果有) if (original.Content != null) { var contentBytes = await original.Content.ReadAsByteArrayAsync(); clone.Content = new RepeatableContent(contentBytes); foreach (var header in original.Content.Headers) { clone.Content.Headers.TryAddWithoutValidation(header.Key, header.Value); } } return clone; } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var originalRequest = request; 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 { // 克隆原请求,避免复用导致的流问题 var retryRequest = await CloneRequest(originalRequest); retryRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponse.AccessToken); return await base.SendAsync(retryRequest, cancellationToken); } catch (Exception e) { _logger.LogError(e, "Post token refresh request retry failed"); return response; } } await _sessionService.EndSessionAndLogout(); return response; }
步骤3:平台适配优化
- Android:可保留自定义
CustomAndroidMessageHandler作为辅助,但核心依赖请求克隆逻辑; - iOS:通过请求克隆+可重复读取的Content,彻底解决
NSUrlSessionHandler不重置流的问题,无需修改原生Handler。
关于Polly支持重试的说明
Polly内部并非真正复用原HttpRequestMessage,而是通过克隆请求创建副本,确保每次重试都有可用的内容流,这也是它能规避流问题的原因。
是否仅Xamarin存在此问题?
不是。原生.NET的HttpClientHandler在部分场景下也会出现流已关闭的问题,但Xamarin的平台特定Handler对请求内容的处理更严格,导致问题更突出。本质原因是.NET官方不支持复用HttpRequestMessage,所有依赖原生网络栈的Handler都会受此限制。
内容的提问来源于stack exchange,提问作者Duncan Tyrell
相关产品推荐
相关产品推荐

