grpc / grpc-dotnet

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

Client attempts to communicate over http1 instead of http2 #518

Closed Earthmark closed 5 years ago

Earthmark commented 5 years ago

I get a feeling this may be an issue with my setup, but I'm consistently seeing this behavior even after wiping the obj and bin directories of my test projects when using pre2 or pre1. I also tried wiping my nuget package caches and re-downloading the packages, this feels like a dramatically bad bug so my guess is some part of my system is borked.

I was trying to do more tests related to #516 and ran into issues where I'm now unable to communicate with a grpc-dotnet client to any grpc server. When running with a go server it just murderizes the connection, but with a kestrel server set to http1Andhttp2 this happens: Status(StatusCode=Cancelled, Detail="Bad gRPC response. Expected HTTP status code 200. Got status code: 426") If the kestrel server is set to only http2 the request fails with: IOException: The response ended prematurely.

I am able to seamlessly communicate with the kestrel server via bloom when set to http1+http2 mode or http2 mode. I haven't tried with a go client but I don't think I'll need to (Feel free to poke if you want me to attempt it though).

I feel I should also mention I'm not setting up tls right now, but the server seems not to care.

Issue: Clients fail to communicate to any grpc server. Expected: The client to talk http2

The code to test with:

greet.proto

syntax = "proto3";

package Greet;

service Greeter {
    rpc Do (Request) returns (Reply);
}

message Request {
}

message Reply {
}

Example client that fails

using System;
using System.Threading.Tasks;
using Greet;
using Grpc.Net.Client;

namespace GrpcTest
{
  public class Program
  {
    public static async Task Main(string[] args)
    {
      // update to whatever your talking to.
      var channel = GrpcChannel.ForAddress(new Uri("http://127.0.0.1:10000"));
      var client = new Greeter.GreeterClient(channel);
      // For me this call fails.
      var response = await client.DoAsync(new Request());
    }
  }
}

Go server (if you want it)

//go:generate protoc --go_out=plugins=grpc:Greet greet.proto

package main

import (
    "context"
    "log"
    "net"

    pb "./Greet"

    "google.golang.org/grpc"
)

type greeterServer struct {
}

func (s *greeterServer) Do(context.Context, *pb.Request) (*pb.Reply, error) {
    return &pb.Reply{}, nil
}

func main() {
    lis, err := net.Listen("tcp", "localhost:10000")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    var opts []grpc.ServerOption
    grpcServer := grpc.NewServer(opts...)
    pb.RegisterGreeterServer(grpcServer, &greeterServer{})
    grpcServer.Serve(lis)
}

Here is also a more complicated integration test case, where it'll spin up a loopback server and play an aggressive game of ping pong with it.

using System;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Greet;
using Grpc.Core;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace GrpcTest
{
  class Program
  {
    static async Task Main()
    {
      using var server = Host.CreateDefaultBuilder().ConfigureWebHostDefaults(webHost =>
      {
        webHost.ConfigureKestrel(kestrel =>
        {
          kestrel.Listen(IPAddress.Loopback, 0, listen =>
          {
            listen.Protocols = HttpProtocols.Http2;
          });
        });
        webHost.ConfigureServices(services =>
        {
          services.AddGrpc();
        });
        webHost.Configure(app =>
        {
          app.UseRouting();
          app.UseEndpoints(endpoints =>
          {
            endpoints.MapGrpcService<GreetService>();
          });
        });
      }).Build();
      await server.StartAsync();

      // Get the IP address
      var internalServer = server.Services.GetRequiredService<IServer>();
      var serverAddress = internalServer.Features.Get<IServerAddressesFeature>().Addresses.First();
      var address = new Uri(serverAddress);

      using var client = Host.CreateDefaultBuilder().ConfigureServices(services =>
      {
        // This client seems to try to communicate through http1, not http2.
        services.AddGrpcClient<Greeter.GreeterClient>(http =>
        {
          http.Address = address;
        });
      }).Build();
      await client.StartAsync();

      var webClient = client.Services.GetRequiredService<Greeter.GreeterClient>();

      for (int i = 0; i < 1000; i++)
      {
        Console.WriteLine("Ping!");
        // For me, this call explodes because of a 426 response code.
        _ = await webClient.DoAsync(new Request());
      }

      await server.StopAsync();
      await client.StopAsync();
    }
  }

  class GreetService : Greeter.GreeterBase
  {
    public override Task<Reply> Do(Request request, ServerCallContext context)
    {
      Console.WriteLine("Pong!");
      return Task.FromResult(new Reply());
    }
  }
}

EDIT: Updated the http1AndHttp2 call to just be http2, as per the comment thread where the and part is not supported, but with http2 this shows the error still.

csproj used for the code files. The client only project only generated a client, but the rest is the same, the behavior didn't change when trying to use the Microsoft.NET.Sdk.Web sdk.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <Protobuf Include="greet.proto" GrpcServices="Server,Client" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Grpc.AspNetCore" Version="0.2.23-pre2" />
  </ItemGroup>

</Project>
JamesNK commented 5 years ago

https://docs.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel?view=aspnetcore-3.0#listenoptionsprotocols

TLS is required if you have set Kestrel to HttpProtocols.Http1AndHttp2. Set it to Http2 or use HTTPS.

Earthmark commented 5 years ago

I just discovered that with bloom as well! It'll only talk to bloom in http2 only mode, but the client still fails to connect to the http2 only service, when the bloom client does succeed.

Earthmark commented 5 years ago

I'm guessing this is part of the prior knowledge mode thing then, where I haven't told the client it's http2. I'll poke at that and see if I can find a resolution.

JamesNK commented 5 years ago

It'll only talk to bloom in http2 only mode, but the client still fails to connect to the http2 only service, when the bloom client does succeed.

The client should connect without TLS when Kestrel is Http2 only.

Earthmark commented 5 years ago

If I'm using a kestrel http2 only server, or the example go server provided in the issue, the grpc client provided by this library fails to connect.

JamesNK commented 5 years ago

Hmmm, ok. That shouldn't happen. What is your OS and dotnet version?

dotnet --info will print out the SDK version you have installed.

JamesNK commented 5 years ago

Wait a second, there is an additional setting required when using HTTP/2 without TLS.

https://docs.microsoft.com/en-us/aspnet/core/grpc/troubleshoot?view=aspnetcore-3.0#call-insecure-grpc-services-with-net-core-client

Are you setting:

// This switch must be set before creating the GrpcChannel/HttpClient.
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
Earthmark commented 5 years ago

That fixed it, adding that line at the start of the test cases does allow the client to connect as expected. Thanks for looking into this, I'll make sure to double check the docs in the future

Earthmark commented 5 years ago

In not on my machine at the moment hence not giving the dotnet info, I don't think it's needed now but in the future I'll remember to provide that by default.