/* * * Copyright 2022 gRPC authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package test import ( "context" "fmt" "net" "testing" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/internal/transport" "google.golang.org/grpc/status" testpb "google.golang.org/grpc/test/grpc_testing" ) func (s) TestHTTPHeaderFrameErrorHandlingHTTPMode(t *testing.T) { type test struct { name string header []string errCode codes.Code } var tests []test // Non-gRPC content-type fallback path. for httpCode := range transport.HTTPStatusConvTab { tests = append(tests, test{ name: fmt.Sprintf("Non-gRPC content-type fallback path with httpCode: %v", httpCode), header: []string{ ":status", fmt.Sprintf("%d", httpCode), "content-type", "text/html", // non-gRPC content type to switch to HTTP mode. "grpc-status", "1", // Make up a gRPC status error "grpc-status-details-bin", "???", // Make up a gRPC field parsing error }, errCode: transport.HTTPStatusConvTab[int(httpCode)], }) } // Missing content-type fallback path. for httpCode := range transport.HTTPStatusConvTab { tests = append(tests, test{ name: fmt.Sprintf("Missing content-type fallback path with httpCode: %v", httpCode), header: []string{ ":status", fmt.Sprintf("%d", httpCode), // Omitting content type to switch to HTTP mode. "grpc-status", "1", // Make up a gRPC status error "grpc-status-details-bin", "???", // Make up a gRPC field parsing error }, errCode: transport.HTTPStatusConvTab[int(httpCode)], }) } // Malformed HTTP status when fallback. tests = append(tests, test{ name: "Malformed HTTP status when fallback", header: []string{ ":status", "abc", // Omitting content type to switch to HTTP mode. "grpc-status", "1", // Make up a gRPC status error "grpc-status-details-bin", "???", // Make up a gRPC field parsing error }, errCode: codes.Internal, }) for _, test := range tests { t.Run(test.name, func(t *testing.T) { serverAddr, cleanup, err := startServer(t, test.header) if err != nil { t.Fatal(err) } defer cleanup() if err := doHTTPHeaderTest(serverAddr, test.errCode); err != nil { t.Error(err) } }) } } // Testing erroneous ResponseHeader or Trailers-only (delivered in the first HEADERS frame). func (s) TestHTTPHeaderFrameErrorHandlingInitialHeader(t *testing.T) { for _, test := range []struct { name string header []string errCode codes.Code }{ { name: "missing gRPC status", header: []string{ ":status", "403", "content-type", "application/grpc", }, errCode: codes.PermissionDenied, }, { name: "malformed grpc-status", header: []string{ ":status", "502", "content-type", "application/grpc", "grpc-status", "abc", }, errCode: codes.Internal, }, { name: "Malformed grpc-tags-bin field", header: []string{ ":status", "502", "content-type", "application/grpc", "grpc-status", "0", "grpc-tags-bin", "???", }, errCode: codes.Unavailable, }, { name: "gRPC status error", header: []string{ ":status", "502", "content-type", "application/grpc", "grpc-status", "3", }, errCode: codes.Unavailable, }, } { t.Run(test.name, func(t *testing.T) { serverAddr, cleanup, err := startServer(t, test.header) if err != nil { t.Fatal(err) } defer cleanup() if err := doHTTPHeaderTest(serverAddr, test.errCode); err != nil { t.Error(err) } }) } } // Testing non-Trailers-only Trailers (delivered in second HEADERS frame) func (s) TestHTTPHeaderFrameErrorHandlingNormalTrailer(t *testing.T) { tests := []struct { name string responseHeader []string trailer []string errCode codes.Code }{ { name: "trailer missing grpc-status", responseHeader: []string{ ":status", "200", "content-type", "application/grpc", }, trailer: []string{ // trailer missing grpc-status ":status", "502", }, errCode: codes.Unavailable, }, { name: "malformed grpc-status-details-bin field with status 404", responseHeader: []string{ ":status", "404", "content-type", "application/grpc", }, trailer: []string{ // malformed grpc-status-details-bin field "grpc-status", "0", "grpc-status-details-bin", "????", }, errCode: codes.Unimplemented, }, { name: "malformed grpc-status-details-bin field with status 200", responseHeader: []string{ ":status", "200", "content-type", "application/grpc", }, trailer: []string{ // malformed grpc-status-details-bin field "grpc-status", "0", "grpc-status-details-bin", "????", }, errCode: codes.Internal, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { serverAddr, cleanup, err := startServer(t, test.responseHeader, test.trailer) if err != nil { t.Fatal(err) } defer cleanup() if err := doHTTPHeaderTest(serverAddr, test.errCode); err != nil { t.Error(err) } }) } } func (s) TestHTTPHeaderFrameErrorHandlingMoreThanTwoHeaders(t *testing.T) { header := []string{ ":status", "200", "content-type", "application/grpc", } serverAddr, cleanup, err := startServer(t, header, header, header) if err != nil { t.Fatal(err) } defer cleanup() if err := doHTTPHeaderTest(serverAddr, codes.Internal); err != nil { t.Fatal(err) } } func startServer(t *testing.T, headerFields ...[]string) (serverAddr string, cleanup func(), err error) { t.Helper() lis, err := net.Listen("tcp", "localhost:0") if err != nil { return "", nil, fmt.Errorf("listening on %q: %v", "localhost:0", err) } server := &httpServer{responses: []httpServerResponse{{trailers: headerFields}}} server.start(t, lis) return lis.Addr().String(), func() { lis.Close() }, nil } func doHTTPHeaderTest(lisAddr string, errCode codes.Code) error { cc, err := grpc.Dial(lisAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { return fmt.Errorf("dial(%q): %v", lisAddr, err) } defer cc.Close() ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() client := testpb.NewTestServiceClient(cc) stream, err := client.FullDuplexCall(ctx) if err != nil { return fmt.Errorf("creating FullDuplex stream: %v", err) } if _, err := stream.Recv(); err == nil || status.Code(err) != errCode { return fmt.Errorf("stream.Recv() = %v, want error code: %v", err, errCode) } return nil }