grpc / grpc-dotnet

gRPC for .NET
Apache License 2.0
4.22k stars 777 forks source link

Setting `HttpVersionPolicy` breaks gRPC client completely #2546

Closed Eagle3386 closed 2 months ago

Eagle3386 commented 2 months ago

What version of gRPC and what language are you using?

ASP.NET Core gRPC-Web service, freshly upgraded to 2.66.0

What operating system (Linux, Windows,...) and version?

Windows 11, latest patch version

What runtime / compiler are you using (e.g. .NET Core SDK version dotnet --info)

.NET SDK 8.0.400

What did you do?

Upgraded the NuGet package from 2.65.0 to 2.66.0 & changed this:

channel.HttpHandler = new GrpcWebHandler(GrpcWebMode.GrpcWeb, new HttpClientHandler()) { HttpVersion = new(2, 0) };

to this:

channel.HttpHandler       = new GrpcWebHandler(GrpcWebMode.GrpcWeb, new HttpClientHandler());
channel.HttpVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;

What did you expect to see?

Client running as before.

What did you see instead?

Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
      Unhandled exception rendering component: Method not found: void Grpc.Net.Client.GrpcChannelOptions.set_HttpVersionPolicy(System.Nullable`1<System.Net.Http.HttpVersionPolicy>)
System.MissingMethodException: Method not found: void Grpc.Net.Client.GrpcChannelOptions.set_HttpVersionPolicy(System.Nullable`1<System.Net.Http.HttpVersionPolicy>)
   at Grpc.Net.ClientFactory.Internal.GrpcCallInvokerFactory.CreateInvoker(EntryKey key)
   at System.Collections.Concurrent.ConcurrentDictionary`2[[Grpc.Net.ClientFactory.Internal.EntryKey, Grpc.Net.ClientFactory, Version=2.0.0.0, Culture=neutral, PublicKeyToken=d754f35622e28bad],[Grpc.Core.CallInvoker, Grpc.Core.Api, Version=2.0.0.0, Culture=neutral, PublicKeyToken=d754f35622e28bad]].GetOrAdd(EntryKey key, Func`2 valueFactory)
   at Grpc.Net.ClientFactory.Internal.GrpcCallInvokerFactory.CreateInvoker(String name, Type type)
   at Grpc.Net.ClientFactory.Internal.DefaultGrpcClientFactory.CreateClient[RedirectionClient](String name)
   at Microsoft.Extensions.DependencyInjection.GrpcClientServiceExtensions.<>c__DisplayClass7_0`1[[Hmd.Services.Redirection.Redirection.RedirectionClient, Hmd.Portal, Version=1.4.0.0, Culture=neutral, PublicKeyToken=null]].<AddGrpcHttpClient>b__0(IServiceProvider s)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitFactory(FactoryCallSite factoryCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2[[Microsoft.Extensions.DependencyInjection.ServiceLookup.RuntimeResolverContext, Microsoft.Extensions.DependencyInjection, Version=8.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60],[System.Object, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].VisitCallSiteMain(ServiceCallSite callSite, RuntimeResolverContext argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitDisposeCache(ServiceCallSite transientCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2[[Microsoft.Extensions.DependencyInjection.ServiceLookup.RuntimeResolverContext, Microsoft.Extensions.DependencyInjection, Version=8.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60],[System.Object, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].VisitCallSite(ServiceCallSite callSite, RuntimeResolverContext argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.RuntimeServiceProviderEngine.<>c__DisplayClass4_0.<RealizeService>b__0(ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType)
   at Microsoft.AspNetCore.Components.ComponentFactory.<>c__DisplayClass9_0.<CreatePropertyInjector>g__Initialize|1(IServiceProvider serviceProvider, IComponent component)
   at Microsoft.AspNetCore.Components.ComponentFactory.InstantiateComponent(IServiceProvider serviceProvider, Type componentType, IComponentRenderMode callerSpecifiedRenderMode, Nullable`1 parentComponentId)
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.InstantiateChildComponentOnFrame(RenderTreeFrame[] frames, Int32 frameIndex, Int32 parentComponentId)
   at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.InitializeNewComponentFrame(DiffContext& diffContext, Int32 frameIndex)
   at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.InitializeNewSubtree(DiffContext& diffContext, Int32 frameIndex)
   at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.InsertNewFrame(DiffContext& diffContext, Int32 newFrameIndex)
   at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForFramesWithSameSequence(DiffContext& diffContext, Int32 oldFrameIndex, Int32 newFrameIndex)
   at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForRange(DiffContext& diffContext, Int32 oldStartIndex, Int32 oldEndIndexExcl, Int32 newStartIndex, Int32 newEndIndexExcl)
   at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForFramesWithSameSequence(DiffContext& diffContext, Int32 oldFrameIndex, Int32 newFrameIndex)
   at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForRange(DiffContext& diffContext, Int32 oldStartIndex, Int32 oldEndIndexExcl, Int32 newStartIndex, Int32 newEndIndexExcl)
   at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForFramesWithSameSequence(DiffContext& diffContext, Int32 oldFrameIndex, Int32 newFrameIndex)
   at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForRange(DiffContext& diffContext, Int32 oldStartIndex, Int32 oldEndIndexExcl, Int32 newStartIndex, Int32 newEndIndexExcl)
   at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForFramesWithSameSequence(DiffContext& diffContext, Int32 oldFrameIndex, Int32 newFrameIndex)
   at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForRange(DiffContext& diffContext, Int32 oldStartIndex, Int32 oldEndIndexExcl, Int32 newStartIndex, Int32 newEndIndexExcl)
   at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.ComputeDiff(Renderer renderer, RenderBatchBuilder batchBuilder, Int32 componentId, ArrayRange`1 oldTree, ArrayRange`1 newTree)
   at Microsoft.AspNetCore.Components.Rendering.ComponentState.RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment, Exception& renderFragmentException)
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.RenderInExistingBatch(RenderQueueEntry renderQueueEntry)
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessRenderQueue() blazor.webassembly.js:1:46958

Anything else we should know about your project / environment?

No, but you're free to ask if necessary.

JamesNK commented 2 months ago

My guess is 2.66.0 isn't correctly being deployed with your app. When this code in another assembly tries to call the new method it can't find it because an older version is present.

Eagle3386 commented 2 months ago

Thanks, @JamesNK! Cleaning & rebuilding the whole solution worked. But even after setting HttpVersion to 3.0 & keeping RequestVersionOrHigher as policy, it still uses HTTP/2 where it's supposed to only use HTTP/3:

Debug: QUIC listener starting with configured endpoint [::1]:4430.
[…]
Debug: Connection id "{id1}" accepted.
Debug: Connection id "{id1}" started.
Debug: Connection {id1} established using the following protocol: Tls13
Information: Request starting HTTP/2 OPTIONS https://localhost:4430/MyService.My/SendData - - -
[…]
Information: Request finished HTTP/2 OPTIONS https://localhost:4430/MyService.My/SendData - 204 - - 59.5485ms
Microsoft.AspNetCore.Hosting.Diagnostics: Information: Request starting HTTP/2 POST
  https://localhost:4430/MyService.My/SendData - application/grpc-web 7
[…]
Information: Request finished HTTP/2 POST https://localhost:4430/MyService.My/SendData - 200 -
  application/grpc-web 637.4950ms
[…]
Debug: Connection id "{id2}" accepted.
Debug: Connection id "{id2}" accepted.
Debug: Connection id "{id2}" started.
[…]
Debug: Stream id "{id2}:00000003" type Unidirectional connected.
Debug: Stream id "{id2}:00000002" type Unidirectional accepted.
Debug: Stream id "{id2}:00000006" type Unidirectional accepted.
Debug: Stream id "{id2}:0000000A" type Unidirectional accepted.
[…]
Debug: Connection id "{id1}", Request id "{id1}:00000011": started reading request body.
Debug: Connection id "{id1}", Request id "{id1}:00000011": done reading request body.
[…]
Debug: Stream id "{id2}:00000002" read timed out.
Debug: Stream id "{id2}:00000003" write timed out.
Debug: Stream id "{id2}:00000003" shutting down writes because: "The connection timed out from inactivity.".
Debug: Stream id "{id2}:00000006" read timed out.
Debug: Stream id "{id2}:0000000A" read timed out.
Debug: Connection id "{id2}" request processing ended abnormally.

System.Net.Quic.QuicException: The connection timed out from inactivity.
   at System.Net.Quic.QuicConnection.HandleEventShutdownInitiatedByTransport(
     _SHUTDOWN_INITIATED_BY_TRANSPORT_e__Struct& data)
   at System.Net.Quic.QuicConnection.HandleConnectionEvent(QUIC_CONNECTION_EVENT& connectionEvent)
   at System.Net.Quic.QuicConnection.NativeCallback(
     QUIC_HANDLE* connection,
     Void* context,
     QUIC_CONNECTION_EVENT* connectionEvent)
--- End of stack trace from previous location ---
   at System.Net.Quic.QuicConnection.AcceptInboundStreamAsync(CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal.QuicConnectionContext.AcceptAsync(
     CancellationToken cancellationToken)
   at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System
     .Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.Http3Connection
     .ProcessRequestsAsync[TContext](IHttpApplication`1 application)

Debug: Connection id "{id2}": GOAWAY stream ID 0.
Debug: Connection id "{id2}" aborted by application with error code -1 because:
  "The HTTP/3 connection faulted.".
Debug: Connection id "{id2}" is closed. The last processed stream ID was (null).
Debug: Connection id "{id2}" stopped.
Debug: Connection id "{id1}" received FIN.
Debug: Connection id "{id1}" is closing.
Debug: The connection queue processing loop for {id1} completed.
Debug: Connection id "{id1}" is closed. The last processed stream ID was 17.
Debug: Connection id "{id1}" sending FIN because: "The Socket transport's send loop completed gracefully."
Debug: Connection id "{id1}" stopped.

Why is that? I really want to test HTTP/3, so that we can adopt it for our gRPC-Web(Text) services ASAP..

JamesNK commented 2 months ago

I'm not sure. Negotiating HTTP/3 involves a number of things to work correctly. Perhaps setting the policy to exact and HTTP/3 in the client might work?

HTTP/3 is only supported on full .NET (not WebAssembly). The server will also need to support HTTP/3.

Eagle3386 commented 2 months ago

Tried even that & it still went HTTP/2 for some requests, e.g., HTTP OPTIONS. Though, the "actual" requests where done in HTTP/3 - and that's with a Blazor WASM standalone client!

As the server is just Kestrel (with NGinX as proxy in production), I set it to that already.

JamesNK commented 2 months ago

I don't think those options have any impact in WebAssembly. In the browser the initial request is often HTTP/2 and then it upgrades to HTTP/3.

Eagle3386 commented 2 months ago

I see. Thanks for clarification, @JamesNK! 👍🏻