Search code examples
c#.netnginxgrpc-web

Connecting to grpc-web via nginx


I am having a golang server that is sending GRPC stream and I am trying to connect to it via windows .NET client but I am only able to do it if I connect to the grpc-web server directly and the connection doesn't work if I use proxy_pass or grpc_pass over nginx to pass through this connection.

This is my nginx configuration :

server {
    listen      8081;
    server_name localhost;

    location / {
        root   /usr/share/nginx/html/app-mobile;
        index  index.html;
        
        add_header Pragma public;
        add_header Cache-Control "public";
        expires $expires;
        etag off;

        # First attempt to serve request as file, then as directory, then fall back to index.html.
        try_files $uri $uri/ /index.html;
    }

    location /api/printerconnection/v1/ {
        proxy_pass http://0.0.0.0:8107/;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

This is how I connect to it from .NET client :

private static async Task waitForOrderGRPCstream()
        {
            var httpHandler = new HttpClientHandler() { UseProxy = true, AllowAutoRedirect = true };
            using var channel = GrpcChannel.ForAddress("http://localhost:3031/api/printerconnection/v1", new GrpcChannelOptions
            {
                HttpHandler = new GrpcWebHandler(httpHandler)
            });
            var client = new PrinterConnectionClient(channel);
            var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5000));
            try
            {
                var myRequest = new GetPrinterConnectionRequest {};
                Console.WriteLine($"Making GRPC Call for streaming");
                using var streamingCall = client.GetPrinterConnectionStream(myRequest, cancellationToken: cts.Token);
                Console.WriteLine($"Will now wait GRPC streaming...");
                while (await streamingCall.ResponseStream.MoveNext(cancellationToken: cts.Token))
                {
                    var response = streamingCall.ResponseStream.Current;
                    // Handle the incoming response from the server.
                    Console.WriteLine($"Received GRPC message: {response.Content}");
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("Stream cancelled because of : "+ex);
            }
        }

The above connection gives error Stream cancelled because of : Grpc.Core.RpcException: Status(StatusCode="Unknown", Detail="Bad gRPC response. HTTP status code: 405") I tried changing to different nginx headers and tried adding different options to the HttpClienthandler on .NET client side but the error persists.

But when I replace http://localhost:8081/api/printerconnection/v1 with http://localhost:8107 to connect to my GRPC web server directly, it works without any issues and I am able to see the content that I send from my server.


Solution

  • The problem turned out to be because services hosted in subdirectories were not supported.

    For more information : https://learn.microsoft.com/en-us/aspnet/core/grpc/troubleshoot?view=aspnetcore-7.0#calling-grpc-services-hosted-in-a-sub-directory

    So I had to add a sub directory handler like this :

    /// <summary>
    /// A delegating handler that adds a subdirectory to the URI of gRPC requests.
    /// </summary>
    public class SubdirectoryHandler : DelegatingHandler
    {
        private readonly string _subdirectory;
    
        public SubdirectoryHandler(HttpMessageHandler innerHandler, string subdirectory)
            : base(innerHandler)
        {
            _subdirectory = subdirectory;
        }
    
        protected override Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request, CancellationToken cancellationToken)
        {
            var old = request.RequestUri;
    
            var url = $"{old.Scheme}://{old.Host}:{old.Port}";
            url += $"{_subdirectory}{request.RequestUri.AbsolutePath}";
            request.RequestUri = new Uri(url, UriKind.Absolute);
    
            return base.SendAsync(request, cancellationToken);
        }
    }
    

    And use this from the code like this :

    var handler = new SubdirectoryHandler(new HttpClientHandler(), "/api/printerconnection/v1");