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