diff --git a/connect_ext_test.go b/connect_ext_test.go index 9c53ba2d..ccdf5418 100644 --- a/connect_ext_test.go +++ b/connect_ext_test.go @@ -19,6 +19,7 @@ import ( "compress/flate" "compress/gzip" "context" + "encoding/binary" "errors" "fmt" "io" @@ -1824,6 +1825,40 @@ func TestUnflushableResponseWriter(t *testing.T) { } } +func TestGRPCErrorMetadataIsTrailersOnly(t *testing.T) { + mux := http.NewServeMux() + mux.Handle(pingv1connect.NewPingServiceHandler(pingServer{})) + server := httptest.NewUnstartedServer(mux) + server.EnableHTTP2 = true + server.StartTLS() + t.Cleanup(server.Close) + + protoBytes, err := proto.Marshal(&pingv1.FailRequest{Code: int32(connect.CodeInternal)}) + assert.Nil(t, err) + // Manually construct a gRPC prefix. Data is uncompressed, so the first byte + // is 0. Set the last 4 bytes to the message length. + var prefix [5]byte + binary.BigEndian.PutUint32(prefix[1:5], uint32(len(protoBytes))) + body := append(prefix[:], protoBytes...) + // Manually send off a gRPC request. + res, err := server.Client().Post( + server.URL+"/connect.ping.v1.PingService/Fail", + "application/grpc", + bytes.NewReader(body), + ) + assert.Nil(t, err) + assert.Equal(t, res.StatusCode, http.StatusOK) + assert.Equal(t, res.Header.Get("Content-Type"), "application/grpc") + // pingServer.Fail adds handlerHeader and handlerTrailer to the error + // metadata. The gRPC protocol should send all error metadata as trailers. + assert.Zero(t, res.Header.Get(handlerHeader)) + assert.Zero(t, res.Header.Get(handlerTrailer)) + _, err = io.Copy(io.Discard, res.Body) + assert.Nil(t, err) + assert.NotZero(t, res.Trailer.Get(handlerHeader)) + assert.NotZero(t, res.Trailer.Get(handlerTrailer)) +} + func TestBidiOverHTTP1(t *testing.T) { t.Parallel() mux := http.NewServeMux() diff --git a/protocol_grpc.go b/protocol_grpc.go index ba5cc9cf..ec2c0afd 100644 --- a/protocol_grpc.go +++ b/protocol_grpc.go @@ -528,8 +528,11 @@ func (hc *grpcHandlerConn) Close(err error) (retErr error) { // has wrapped the response writer in net/http middleware that doesn't // implement http.Flusher, we must pre-declare our HTTP trailers. We can // remove this when Go 1.21 ships and we drop support for Go 1.19. - for key, values := range mergedTrailers { + for key := range mergedTrailers { hc.responseWriter.Header().Add("Trailer", key) + } + hc.responseWriter.WriteHeader(http.StatusOK) + for key, values := range mergedTrailers { for _, value := range values { hc.responseWriter.Header().Add(key, value) }