From 11c87090d2e8e655762efa86be59728256fc42f4 Mon Sep 17 00:00:00 2001 From: Akshay Shah Date: Thu, 24 Aug 2023 23:09:56 -0400 Subject: [PATCH] Set X-User-Agent in gRPC-Web clients The gRPC-Web pseudo-specification isn't totally clear, but it sounds like it requires setting `X-User-Agent` instead of `User-Agent` as a part of the protocol. This makes sense in browsers, where JS _can't_ set `User-Agent`, but it's very odd for backend clients. This PR proposes splitting the difference and setting both headers. --- connect_ext_test.go | 25 +++++++++++++++++++++++++ protocol_grpc.go | 9 +++++++++ 2 files changed, 34 insertions(+) diff --git a/connect_ext_test.go b/connect_ext_test.go index 4545071c..030c47d3 100644 --- a/connect_ext_test.go +++ b/connect_ext_test.go @@ -2020,6 +2020,31 @@ func TestAllowCustomUserAgent(t *testing.T) { } } +func TestWebXUserAgent(t *testing.T) { + t.Parallel() + + mux := http.NewServeMux() + mux.Handle(pingv1connect.NewPingServiceHandler(&pluggablePingServer{ + ping: func(_ context.Context, req *connect.Request[pingv1.PingRequest]) (*connect.Response[pingv1.PingResponse], error) { + agent := req.Header().Get("User-Agent") + assert.NotZero(t, agent) + assert.Equal( + t, + req.Header().Get("X-User-Agent"), + agent, + ) + return connect.NewResponse(&pingv1.PingResponse{Number: req.Msg.Number}), nil + }, + })) + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + client := pingv1connect.NewPingServiceClient(server.Client(), server.URL, connect.WithGRPCWeb()) + req := connect.NewRequest(&pingv1.PingRequest{Number: 42}) + _, err := client.Ping(context.Background(), req) + assert.Nil(t, err) +} + func TestBidiOverHTTP1(t *testing.T) { t.Parallel() mux := http.NewServeMux() diff --git a/protocol_grpc.go b/protocol_grpc.go index 44dfaf69..8a9d0ada 100644 --- a/protocol_grpc.go +++ b/protocol_grpc.go @@ -49,6 +49,8 @@ const ( grpcWebContentTypeDefault = "application/grpc-web" grpcContentTypePrefix = grpcContentTypeDefault + "+" grpcWebContentTypePrefix = grpcWebContentTypeDefault + "+" + + headerXUserAgent = "X-User-Agent" ) var ( @@ -252,6 +254,13 @@ func (g *grpcClient) WriteRequestHeader(_ StreamType, header http.Header) { if getHeaderCanonical(header, headerUserAgent) == "" { header[headerUserAgent] = []string{defaultGrpcUserAgent} } + if g.web && getHeaderCanonical(header, headerXUserAgent) == "" { + // The gRPC-Web pseudo-specification seems to require X-User-Agent rather + // than User-Agent for all clients, even if they're not browser-based. This + // is very odd for a backend client, so we'll split the difference and set + // both. + header[headerXUserAgent] = []string{defaultGrpcUserAgent} + } header[headerContentType] = []string{grpcContentTypeFromCodecName(g.web, g.Codec.Name())} // gRPC handles compression on a per-message basis, so we don't want to // compress the whole stream. By default, http.Client will ask the server