You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 00:37:18