doctorseus/grpc-dotnet-unity

headers are not making their way back to the Grpc library

mbruce opened this issue · 5 comments

When doing a simple unary call, I can see the server sending the response correctly. I can also debug this library and see that the grpc-status is in the trailing header.

That is coming in to GRPCBestHttpHandler, and the bestRequest.OnHeadersReceived gets the trailing headers and these are set with grpcResponseMessage.TrailingHeaders.

However, Grpc library is getting:

Exception: Grpc.Core.RpcException: Status(StatusCode="Cancelled", Detail="No grpc-status found on response.")

Full stack from GRPC is:

Exception: Grpc.Core.RpcException: Status(StatusCode="Cancelled", Detail="No grpc-status found on response.")
  at Grpc.Net.Client.Internal.HttpClientCallInvoker.BlockingUnaryCall[TRequest,TResponse] (Grpc.Core.Method`2[TRequest,TResponse] method, System.String host, Grpc.Core.CallOptions options, TRequest request) [0x00016] in <d97e013995eb4d27a7e6fafbdd95d13e>:0 
  at Grpc.Core.Interceptors.InterceptingCallInvoker.<BlockingUnaryCall>b__3_0[TRequest,TResponse] (TRequest req, Grpc.Core.Interceptors.ClientInterceptorContext`2[TRequest,TResponse] ctx) [0x0001b] in <18da7575174844b1b40d2cd0a1e5ca02>:0 
  at Grpc.Core.ClientBase+ClientBaseConfiguration+ClientBaseConfigurationInterceptor.BlockingUnaryCall[TRequest,TResponse] (TRequest request, Grpc.Core.Interceptors.ClientInterceptorContext`2[TRequest,TResponse] context, Grpc.Core.Interceptors.Interceptor+BlockingUnaryCallContinuation`2[TRequest,TResponse] continuation) [0x00009] in <18da7575174844b1b40d2cd0a1e5ca02>:0 
  at Grpc.Core.Interceptors.InterceptingCallInvoker.BlockingUnaryCall[TRequest,TResponse] (Grpc.Core.Method`2[TRequest,TResponse] method, System.String host, Grpc.Core.CallOptions options, TRequest request) [0x00010] in <18da7575174844b1b40d2cd0a1e5ca02>:0 

Hi @mbruce , what grpc server implementation are you using (and which version)?
(I assume I only verified unary calls where the status is already in the answer given that the answer is small and buffered.)

server is in java, and I'm using NettyServerBuilder (version: io.grpc:grpc-netty:1.49.2). the call is very small. pretty much a hello world.

service TestService {
  rpc JoinGame(JoinGameRequest) returns (JoinGameResponse) {}
}

message JoinGameRequest {
  string game_id = 1;
  string player_id = 2;
  string skin_name = 3;
  float spawn_x = 4;
  float spawn_y = 5;
}

message JoinGameResponse {
  bool success = 1;
  // if failed, the reason for failure
  optional string error_reason = 2;
}

Digging through GRPC source I found that it is expecting to find trailing headers in grpcResponseMessage.RequestMessage.Properties["__ResponseTrailers"] so I made this private class in GRPCBestHttpHandler:

private class ResponseTrailers : HttpHeaders {}

and then I modified GRPCBestHttpHandler like this:

// Write incoming headers OR trailing headers
bestRequest.OnHeadersReceived += (HTTPRequest _, HTTPResponse response, Dictionary<string, List<string>> headers) =>
{
	// Only leading header contains :status
	bool isLeadingHeader = headers.Keys.Contains(":status");

	ResponseTrailers trailers = null;
	if (!isLeadingHeader) trailers = new ResponseTrailers(); // <------- Added line
	foreach (KeyValuePair<string, List<string>> kvp in headers)
	{
		// Either leading or trailing header
		if (isLeadingHeader)
		{
			grpcResponseMessage.Content.Headers.TryAddWithoutValidation(kvp.Key, kvp.Value);
		}
		else
		{
			trailers.Add(kvp.Key, kvp.Value); // <------- Added line
			grpcResponseMessage.TrailingHeaders.TryAddWithoutValidation(kvp.Key, kvp.Value);
		}
	}

	if (isLeadingHeader)
	{
		// Copy HTTP status fields
		grpcResponseMessage.ReasonPhrase = response.Message;
		grpcResponseMessage.StatusCode = (HttpStatusCode) response.StatusCode;
		grpcResponseMessage.Version = new Version(response.VersionMajor, response.VersionMinor);
	}
	// If it is a trailing header and contains grpc-status we reached EOF
	else if (headers.Keys.Contains("grpc-status"))
	{
		incomingDataStream.Close();
		grpcResponseMessage.RequestMessage.Properties["__ResponseTrailers"] = trailers; // <------- Added line
	}
};

The GRPC code that handles this can be seen here: https://github.com/grpc/grpc-dotnet/blob/master/src/Shared/TrailingHeadersHelpers.cs. It seems the problem is actually that we installed .netstandard2 dlls and we could have installed .nestandard2.1 since latest LTS Unity 2021.3 support it.

Thanks @igor84 for figuring this out. I also verified that the reported issue happens only when you use netstandard2.0 of https://www.nuget.org/packages/Grpc.Net.Client/2.50.0. So updating to netstandard2.1 should fix your issue @mbruce.

In general, the versions of all dependencies this code was tested on are mentioned here: https://github.com/doctorseus/grpc-dotnet-unity#version-100