如何在从Cloud Run的网页实例发起HTTP请求时,防止出现“不支持的操作”异常?
我在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解析路径。
我建议的修复顺序:
- 检查运行中的Cloud Run容器内的实际运行时版本 (
dotnet --info)。如果版本 < 8.0.2,请修复基础镜像——这是根本的修复,其他措施都将无关紧要。固定的aspnet:8.0.1标签、自建的基础镜像,或带有旧运行时打包的自包含发布,通常是罪魁祸首。 - 如果你无法摆脱旧运行时,请在GCP前端禁用HTTP/3广告,使用
quicOverride=DISABLE。根据Google的 External Application Load Balancer文档,这会使负载均衡器完全不再发送alt-svc头,从而移除触发条件。 - 你的
NetworkInformationException重试是一种合理的安全网,但在完成上述1) 或2) 之后就不再需要。