如何在从Cloud Run的网页实例发起HTTP请求时,防止出现“不支持的操作”异常?

后端开发 2026-07-10

我在Google Cloud Run上运行一个ASP.NET Core 8的 Web API。它使用 HttpClient 调用通过Google Cloud HTTP(S) 负载均衡器公开的另一内部API。

负载均衡器会添加以下头部:

Alt-Svc: h3=":443"; ma=2592000, h3-29=":443"; ma=2592000

问题

请求偶发地因以下错误失败:

System.Net.NetworkInformation.NetworkInformationException (95): Operation not supported
   at System.Net.NetworkInformation.NetworkChange.CreateSocket()
   at System.Net.NetworkInformation.NetworkChange.add_NetworkAddressChanged(NetworkAddressChangedEventHandler value)
   at System.Net.Http.HttpConnectionPoolManager.StartMonitoringNetworkChanges()
   at System.Net.Http.HttpConnectionPool.HandleAltSvc(IEnumerable`1 altSvcHeaderValues, Nullable`1 responseAge)
   at System.Net.Http.HttpConnectionPool.ProcessAltSvc(HttpResponseMessage response)
   at System.Net.Http.HttpConnectionPool.SendWithVersionDetectionAndRetryAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken)
   at System.Net.Http.DiagnosticsHandler.SendAsyncCore(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
   at Microsoft.Extensions.Http.Logging.LoggingHttpMessageHandler.<SendCoreAsync>g__Core|4_0(HttpRequestMessage request, Boolean useAsync, CancellationToken cancellationToken)
   at Microsoft.Extensions.Http.PolicyHttpMessageHandler.SendCoreAsync(HttpRequestMessage request, Context context, CancellationToken cancellationToken)
   at Polly.Retry.AsyncRetryEngine.ImplementationAsync[TResult](Func`3 action, Context context, ExceptionPredicates shouldRetryExceptionPredicates, ResultPredicates`1 shouldRetryResultPredicates, Func`5 onRetryAsync, CancellationToken cancellationToken, Int32 permittedRetryCount, IEnumerable`1 sleepDurationsEnumerable, Func`4 sleepDurationProvider, Boolean continueOnCapturedContext)
   at Polly.AsyncPolicy`1.ExecuteInternalAsync(Func`3 action, Context context, Boolean continueOnCapturedContext, CancellationToken cancellationToken)
   at Microsoft.Extensions.Http.PolicyHttpMessageHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
   at Redacted.Signon.Client.Service.SusMessageHandler.<>c__DisplayClass3_0.<<SendAsync>b__2>d.MoveNext() in /workspace/Redacted.Billing/Redacted.Signon.Client/Service/SusMessageHandler.cs:line 51
--- End of stack trace from previous location ---
   at Polly.Retry.AsyncRetryEngine.ImplementationAsync[TResult](Func`3 action, Context context, ExceptionPredicates shouldRetryExceptionPredicates, ResultPredicates`1 shouldRetryResultPredicates, Func`5 onRetryAsync, CancellationToken cancellationToken, Int32 permittedRetryCount, IEnumerable`1 sleepDurationsEnumerable, Func`4 sleepDurationProvider, Boolean continueOnCapturedContext)
   at Polly.AsyncPolicy`1.ExecuteInternalAsync(Func`3 action, Context context, Boolean continueOnCapturedContext, CancellationToken cancellationToken)
   at Redacted.Signon.Client.Service.SusMessageHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) in /workspace/Redacted.Billing/Redacted.Signon.Client/Service/SusMessageHandler.cs:line 44
   at Microsoft.Extensions.Http.Logging.LoggingScopeHttpMessageHandler.<SendCoreAsync>g__Core|4_0(HttpRequestMessage request, Boolean useAsync, CancellationToken cancellationToken)
   at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)
   at Redacted.Signon.Client.Service.SusService.GetUsersByIds(IEnumerable`1 susUserIds, Boolean includeProviderPictureUrls, CancellationToken cancellationToken) in /workspace/Redacted.Billing/Redacted.Signon.Client/Service/SusService.cs:line 114
   at Redacted.Signon.Client.Service.SusService.GetUserById(String userId, Boolean includeProviderPictureUrls, CancellationToken cancellationToken) in /workspace/Redacted.Billing/Redacted.Signon.Client/Service/SusService.cs:line 126
   at Redacted.Billing.Services.Authentication.TokenValidationService.ValidateUserTokenAsync(IServiceProvider services, ClaimsPrincipal principal, ClaimsIdentity claimsIdentity, CancellationToken cancellationToken) in /workspace/Redacted.Billing/Redacted.Billing/Services/Authentication/TokenValidationService.cs:line 101
   at Redacted.Billing.Services.Authentication.TokenValidationService.ValidateTokenAsync(IServiceProvider services, ClaimsPrincipal principal, CancellationToken cancellationToken) in /workspace/Redacted.Billing/Redacted.Billing/Services/Authentication/TokenValidationService.cs:line 35
   at Redacted.Billing.App_Start.Services.Auth.SusConfiguration.<PostConfigure>b__4_0(TokenValidatedContext ctx) in /workspace/Redacted.Billing/Redacted.Billing/App_Start/Services/Auth.cs:line 77
   at Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.HandleAuthenticateAsync()

从堆栈跟踪看,异常发生在 HttpClient 处理 Alt-Svc 头部时。

观察结果

  • 这仅在Cloud Run上发生(无法在本地复现)
  • 请求在HTTP/2下通常也能成功
  • 故障偶发,可能与 HttpClient 内部的Alt-Svc缓存 / 退避行为有关
  • 异常在 HttpClient 开始监控网络变化(通过 NetworkChange)时抛出

HttpClient配置

services
    .AddHttpClient(Constants.RedactedApiClient)
    .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler()
    {
        AllowAutoRedirect = false
    })
    .ConfigureHttpClient(client =>
    {
        client.DefaultRequestVersion = HttpVersion.Version20;
        client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrLower;
    });

用法:

var client = factory.CreateClient(Constants.RedactedApiClient);
using var response = await client.GetAsync(url, cancellationToken);
response.EnsureSuccessStatusCode();

我已尝试的方法

  • 强制使用HTTP/2(见上面的代码):

DefaultRequestVersion = HttpVersion.Version20; DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrLower; * 通过环境变量禁用HTTP/3:

DOTNET_SYSTEM_NET_HTTP_SOCKETSHTTPHANDLER_HTTP3SUPPORT=false * 回顾相关问题:
https://github.com/dotnet/runtime/issues/94794

以上都无法阻止异常。

如何解决这个问题,使请求能够正常流转?目前,我已经在 NetworkInformationException 抛出时增加了重试。

解决方案

这并不是一个 IHttpClientFactory 问题。它与在 dotnet/runtime#94794 跟踪的.NET 8回归相符,失败路径经过 ProcessAltSvc -> HandleAltSvc -> StartMonitoringNetworkChanges,原始重现是在Google Cloud上使用简单的 new HttpClient(),没有工厂参与。

微软在 [PR #94861] 通过将 NetworkChange 订阅包装在一个 try/catch 中来修复(RFC 7838允许跳过网络变化监控),并在 [PR #95123] 将其回移到.NET 8,随 8.0.2(2024年 2月的服务版本)发行。

回溯说明中的两个细节直接与你的情况相关:
客户影响部分指出,该回归影响任何也宣告HTTP/3的服务器的HTTP请求,包含HTTP/1.1或 2.0。因此:

DefaultRequestVersion = HttpVersion.Version20;
DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrLower;

无法避免这个错误,崩溃发生在对 Alt-Svc 头部进行解析时,在任何HTTP/3协商之前。之所以成为回归,是因为 #83494:在.NET 7中,问题的检查在 msquic.so 存在性检查之后执行;在.NET 8中顺序改变,因此 NetworkChange 的订阅现在也会针对HTTP/1.1和 HTTP/2请求运行。

这也解释了为什么 DOTNET_SYSTEM_NET_HTTP_SOCKETSHTTPHANDLER_HTTP3SUPPORT=false 在未打补丁的运行时上没有帮助——禁用客户端的HTTP/3也不会抑制Alt-Svc解析路径。

我建议的修复顺序:

  1. 检查运行中的Cloud Run容器内的实际运行时版本 (dotnet --info)。如果版本 < 8.0.2,请修复基础镜像——这是根本的修复,其他措施都将无关紧要。固定的 aspnet:8.0.1 标签、自建的基础镜像,或带有旧运行时打包的自包含发布,通常是罪魁祸首。
  2. 如果你无法摆脱旧运行时,请在GCP前端禁用HTTP/3广告,使用 quicOverride=DISABLE。根据Google的 External Application Load Balancer文档,这会使负载均衡器完全不再发送 alt-svc 头,从而移除触发条件。
  3. 你的 NetworkInformationException 重试是一种合理的安全网,但在完成上述1) 或2) 之后就不再需要。
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章