blob: d2005de49bf134a0bd13770048e7677ea2b51062 [file] [log] [blame]
// Copyright 2016 The LUCI 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 prpc
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strconv"
"testing"
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/encoding/prototext"
"google.golang.org/protobuf/proto"
"go.chromium.org/luci/common/testing/ftt"
"go.chromium.org/luci/common/testing/truth/assert"
"go.chromium.org/luci/common/testing/truth/should"
"go.chromium.org/luci/server/router"
"go.chromium.org/luci/grpc/prpc/internal/testpb"
)
type greeterService struct {
testpb.UnimplementedGreeterServer
headerMD metadata.MD
errDetails *errdetails.DebugInfo
}
func (s *greeterService) SayHello(ctx context.Context, req *testpb.HelloRequest) (*testpb.HelloReply, error) {
if req.Name == "" {
return nil, status.Errorf(codes.InvalidArgument, "Name unspecified")
}
if s.errDetails != nil {
st, err := status.New(codes.Unknown, "").WithDetails(s.errDetails)
if err == nil {
err = st.Err()
}
return nil, err
}
if s.headerMD != nil {
SetHeader(ctx, s.headerMD)
}
return &testpb.HelloReply{
Message: "Hello " + req.Name,
}, nil
}
type calcService struct {
testpb.UnimplementedCalcServer
}
func (s *calcService) Multiply(ctx context.Context, req *testpb.MultiplyRequest) (*testpb.MultiplyResponse, error) {
return &testpb.MultiplyResponse{
Z: req.X & req.Y,
}, nil
}
func decodeRequest(body []byte) *testpb.HelloRequest {
msg := &testpb.HelloRequest{}
if err := prototext.Unmarshal(body, msg); err != nil {
panic(err)
}
return msg
}
func decodeReply(body []byte) *testpb.HelloReply {
msg := &testpb.HelloReply{}
if err := prototext.Unmarshal(body, msg); err != nil {
panic(err)
}
return msg
}
func TestServer(t *testing.T) {
t.Parallel()
ftt.Run("Greeter service", t, func(t *ftt.Test) {
server := Server{MaxRequestSize: 100}
greeterSvc := &greeterService{}
testpb.RegisterGreeterServer(&server, greeterSvc)
t.Run("Register Calc service", func(t *ftt.Test) {
testpb.RegisterCalcServer(&server, &calcService{})
assert.Loosely(t, server.ServiceNames(), should.Resemble([]string{
"prpc.Calc",
"prpc.Greeter",
}))
})
t.Run("Handlers", func(t *ftt.Test) {
c := context.Background()
r := router.New()
server.InstallHandlers(r, router.NewMiddlewareChain(
func(ctx *router.Context, next router.Handler) {
ctx.Request = ctx.Request.WithContext(c)
next(ctx)
},
))
res := httptest.NewRecorder()
hiMsg := bytes.NewBufferString(`name: "Lucy"`)
req, err := http.NewRequest("POST", "/prpc/prpc.Greeter/SayHello", hiMsg)
assert.Loosely(t, err, should.BeNil)
req.Header.Set("Content-Type", mtPRPCText)
strCode := func(c codes.Code) string {
return strconv.Itoa(int(c))
}
t.Run("Works", func(t *ftt.Test) {
req.Header.Set("Accept", mtPRPCText)
r.ServeHTTP(res, req)
assert.Loosely(t, res.Code, should.Equal(http.StatusOK))
assert.Loosely(t, res.Result().Header, should.Resemble(http.Header{
"Content-Type": {"application/prpc; encoding=text"},
"Date": nil,
"X-Content-Type-Options": {"nosniff"},
"X-Prpc-Grpc-Code": {strCode(codes.OK)},
}))
assert.Loosely(t, decodeReply(res.Body.Bytes()), should.Resemble(&testpb.HelloReply{Message: "Hello Lucy"}))
})
t.Run("Header Metadata", func(t *ftt.Test) {
greeterSvc.headerMD = metadata.Pairs("a", "1", "b", "2", "date", "2112")
r.ServeHTTP(res, req)
assert.Loosely(t, res.Code, should.Equal(http.StatusOK))
assert.Loosely(t, res.Result().Header, should.Resemble(http.Header{
"A": {"1"},
"B": {"2"},
"Content-Type": {"application/prpc; encoding=binary"},
"Date": {"2112"},
"X-Content-Type-Options": {"nosniff"},
"X-Prpc-Grpc-Code": {strCode(codes.OK)},
}))
})
t.Run("Status details", func(t *ftt.Test) {
greeterSvc.errDetails = &errdetails.DebugInfo{Detail: "x"}
r.ServeHTTP(res, req)
assert.Loosely(t, res.Result().Header, should.Resemble(http.Header{
"Content-Type": {"text/plain; charset=utf-8"},
"Date": nil,
"X-Content-Type-Options": {"nosniff"},
"X-Prpc-Grpc-Code": {strCode(codes.Unknown)},
"X-Prpc-Status-Details-Bin": {"Cih0eXBlLmdvb2dsZWFwaXMuY29tL2dvb2dsZS5ycGMuRGVidWdJbmZvEgMSAXg="},
}))
})
t.Run("Invalid Accept header", func(t *ftt.Test) {
req.Header.Set("Accept", "blah")
r.ServeHTTP(res, req)
assert.Loosely(t, res.Code, should.Equal(http.StatusNotAcceptable))
assert.Loosely(t, res.Result().Header, should.Resemble(http.Header{
"Content-Type": {"text/plain; charset=utf-8"},
"Date": nil,
"X-Content-Type-Options": {"nosniff"},
"X-Prpc-Grpc-Code": {strCode(codes.InvalidArgument)},
}))
})
t.Run("Invalid max response size header", func(t *ftt.Test) {
for _, bad := range []string{"not-an-int", "0", "-1", " 123"} {
req.Header.Set(HeaderMaxResponseSize, bad)
r.ServeHTTP(res, req)
assert.Loosely(t, res.Code, should.Equal(http.StatusBadRequest))
assert.Loosely(t, res.Result().Header, should.Resemble(http.Header{
"Content-Type": {"text/plain; charset=utf-8"},
"Date": nil,
"X-Content-Type-Options": {"nosniff"},
"X-Prpc-Grpc-Code": {strCode(codes.InvalidArgument)},
}))
}
})
t.Run("Invalid header", func(t *ftt.Test) {
req.Header.Set("X-Bin", "zzz")
r.ServeHTTP(res, req)
assert.Loosely(t, res.Code, should.Equal(http.StatusBadRequest))
assert.Loosely(t, res.Result().Header, should.Resemble(http.Header{
"Content-Type": {"text/plain; charset=utf-8"},
"Date": nil,
"X-Content-Type-Options": {"nosniff"},
"X-Prpc-Grpc-Code": {strCode(codes.InvalidArgument)},
}))
})
t.Run("Malformed request message", func(t *ftt.Test) {
hiMsg.WriteString("\nblah")
r.ServeHTTP(res, req)
assert.Loosely(t, res.Code, should.Equal(http.StatusBadRequest))
assert.Loosely(t, res.Result().Header, should.Resemble(http.Header{
"Content-Type": {"text/plain; charset=utf-8"},
"Date": nil,
"X-Content-Type-Options": {"nosniff"},
"X-Prpc-Grpc-Code": {strCode(codes.InvalidArgument)},
}))
})
t.Run("Invalid request message", func(t *ftt.Test) {
hiMsg.Reset()
r.ServeHTTP(res, req)
assert.Loosely(t, res.Code, should.Equal(http.StatusBadRequest))
assert.Loosely(t, res.Result().Header, should.Resemble(http.Header{
"Content-Type": {"text/plain; charset=utf-8"},
"Date": nil,
"X-Content-Type-Options": {"nosniff"},
"X-Prpc-Grpc-Code": {strCode(codes.InvalidArgument)},
}))
assert.Loosely(t, res.Body.String(), should.Equal("Name unspecified\n"))
})
t.Run("no such service", func(t *ftt.Test) {
req.URL.Path = "/prpc/xxx/SayHello"
r.ServeHTTP(res, req)
assert.Loosely(t, res.Code, should.Equal(http.StatusNotImplemented))
assert.Loosely(t, res.Result().Header, should.Resemble(http.Header{
"Content-Type": {"text/plain; charset=utf-8"},
"Date": nil,
"X-Content-Type-Options": {"nosniff"},
"X-Prpc-Grpc-Code": {strCode(codes.Unimplemented)},
}))
})
t.Run("no such method", func(t *ftt.Test) {
req.URL.Path = "/prpc/Greeter/xxx"
r.ServeHTTP(res, req)
assert.Loosely(t, res.Code, should.Equal(http.StatusNotImplemented))
assert.Loosely(t, res.Result().Header, should.Resemble(http.Header{
"Content-Type": {"text/plain; charset=utf-8"},
"Date": nil,
"X-Content-Type-Options": {"nosniff"},
"X-Prpc-Grpc-Code": {strCode(codes.Unimplemented)},
}))
})
t.Run(`When access control is enabled without credentials`, func(t *ftt.Test) {
server.AccessControl = func(ctx context.Context, origin string) AccessControlDecision {
return AccessControlDecision{
AllowCrossOriginRequests: true,
AllowCredentials: false,
}
}
req.Header.Add("Origin", "http://example.com")
r.ServeHTTP(res, req)
assert.Loosely(t, res.Code, should.Equal(http.StatusOK))
assert.Loosely(t, res.Result().Header, should.Resemble(http.Header{
"Access-Control-Allow-Origin": {"http://example.com"},
"Access-Control-Expose-Headers": {"X-Prpc-Grpc-Code, X-Prpc-Status-Details-Bin"},
"Content-Type": {"application/prpc; encoding=binary"},
"Date": nil,
"Vary": {"Origin"},
"X-Content-Type-Options": {"nosniff"},
"X-Prpc-Grpc-Code": {strCode(codes.OK)},
}))
})
t.Run(`When access control is enabled for "http://example.com"`, func(t *ftt.Test) {
decision := AccessControlDecision{
AllowCrossOriginRequests: true,
AllowCredentials: true,
}
server.AccessControl = func(ctx context.Context, origin string) AccessControlDecision {
if origin == "http://example.com" {
return decision
}
return AccessControlDecision{}
}
t.Run(`When sending an OPTIONS request`, func(t *ftt.Test) {
req.Method = "OPTIONS"
t.Run(`Will supply Access-* headers to "http://example.com"`, func(t *ftt.Test) {
req.Header.Add("Origin", "http://example.com")
r.ServeHTTP(res, req)
assert.Loosely(t, res.Code, should.Equal(http.StatusOK))
assert.Loosely(t, res.Result().Header, should.Resemble(http.Header{
"Access-Control-Allow-Credentials": {"true"},
"Access-Control-Allow-Headers": {"Origin, Content-Type, Accept, Authorization, X-Prpc-Grpc-Timeout, X-Prpc-Max-Response-Size"},
"Access-Control-Allow-Methods": {"OPTIONS, POST"},
"Access-Control-Allow-Origin": {"http://example.com"},
"Access-Control-Max-Age": {"600"},
"Vary": {"Origin"},
}))
})
t.Run(`Will not supply access-* headers to "http://foo.bar"`, func(t *ftt.Test) {
req.Header.Add("Origin", "http://foo.bar")
r.ServeHTTP(res, req)
assert.Loosely(t, res.Code, should.Equal(http.StatusOK))
assert.Loosely(t, res.Result().Header, should.Resemble(http.Header{}))
})
})
t.Run(`When sending a POST request`, func(t *ftt.Test) {
t.Run(`Will supply Access-* headers to "http://example.com"`, func(t *ftt.Test) {
req.Header.Add("Origin", "http://example.com")
r.ServeHTTP(res, req)
assert.Loosely(t, res.Code, should.Equal(http.StatusOK))
assert.Loosely(t, res.Result().Header, should.Resemble(http.Header{
"Access-Control-Allow-Credentials": {"true"},
"Access-Control-Allow-Origin": {"http://example.com"},
"Access-Control-Expose-Headers": {"X-Prpc-Grpc-Code, X-Prpc-Status-Details-Bin"},
"Content-Type": {"application/prpc; encoding=binary"},
"Date": nil,
"Vary": {"Origin"},
"X-Content-Type-Options": {"nosniff"},
"X-Prpc-Grpc-Code": {strCode(codes.OK)},
}))
})
t.Run(`Will not supply access-* headers to "http://foo.bar"`, func(t *ftt.Test) {
req.Header.Add("Origin", "http://foo.bar")
r.ServeHTTP(res, req)
assert.Loosely(t, res.Code, should.Equal(http.StatusOK))
assert.Loosely(t, res.Result().Header, should.Resemble(http.Header{
"Content-Type": {"application/prpc; encoding=binary"},
"Date": nil,
"X-Content-Type-Options": {"nosniff"},
"X-Prpc-Grpc-Code": {strCode(codes.OK)},
}))
})
})
t.Run(`Using custom AllowHeaders`, func(t *ftt.Test) {
decision.AllowHeaders = []string{"Booboo", "bobo"}
req.Method = "OPTIONS"
req.Header.Add("Origin", "http://example.com")
r.ServeHTTP(res, req)
assert.Loosely(t, res.Code, should.Equal(http.StatusOK))
assert.Loosely(t, res.Result().Header, should.Resemble(http.Header{
"Access-Control-Allow-Credentials": {"true"},
"Access-Control-Allow-Headers": {"Booboo, bobo, Origin, Content-Type, Accept, Authorization, X-Prpc-Grpc-Timeout, X-Prpc-Max-Response-Size"},
"Access-Control-Allow-Methods": {"OPTIONS, POST"},
"Access-Control-Allow-Origin": {"http://example.com"},
"Access-Control-Max-Age": {"600"},
"Vary": {"Origin"},
}))
})
})
t.Run("Override callback: pass through", func(t *ftt.Test) {
req.Header.Set("Accept", mtPRPCText)
called := false
server.RegisterOverride("prpc.Greeter", "SayHello",
func(rw http.ResponseWriter, req *http.Request, body func(msg proto.Message) error) (bool, error) {
called = true
return false, nil
},
)
r.ServeHTTP(res, req)
assert.Loosely(t, res.Code, should.Equal(http.StatusOK))
assert.Loosely(t, res.Result().Header, should.Resemble(http.Header{
"Content-Type": {"application/prpc; encoding=text"},
"Date": nil,
"X-Content-Type-Options": {"nosniff"},
"X-Prpc-Grpc-Code": {strCode(codes.OK)},
}))
assert.Loosely(t, decodeReply(res.Body.Bytes()), should.Resemble(&testpb.HelloReply{Message: "Hello Lucy"}))
assert.Loosely(t, called, should.BeTrue)
})
t.Run("Override callback: override", func(t *ftt.Test) {
req.Header.Set("Accept", mtPRPCText)
var rawBody []byte
server.RegisterOverride("prpc.Greeter", "SayHello",
func(rw http.ResponseWriter, req *http.Request, body func(msg proto.Message) error) (bool, error) {
rawBody, _ = io.ReadAll(req.Body)
rw.Header().Set("Overridden", "1")
_, _ = fmt.Fprintf(rw, "Override")
return true, nil
},
)
r.ServeHTTP(res, req)
assert.Loosely(t, res.Code, should.Equal(http.StatusOK))
assert.Loosely(t, res.Result().Header, should.Resemble(http.Header{
"Content-Type": {"text/plain; charset=utf-8"},
"Overridden": {"1"},
}))
assert.Loosely(t, res.Body.String(), should.Equal("Override"))
assert.Loosely(t, decodeRequest(rawBody), should.Resemble(&testpb.HelloRequest{Name: "Lucy"}))
})
t.Run("Override callback: error", func(t *ftt.Test) {
req.Header.Set("Accept", mtPRPCText)
server.RegisterOverride("prpc.Greeter", "SayHello",
func(rw http.ResponseWriter, req *http.Request, body func(msg proto.Message) error) (bool, error) {
_, _ = io.ReadAll(req.Body)
return false, errors.New("BOOM")
},
)
r.ServeHTTP(res, req)
assert.Loosely(t, res.Code, should.Equal(http.StatusBadRequest))
assert.Loosely(t, res.Result().Header, should.Resemble(http.Header{
"Content-Type": {"text/plain; charset=utf-8"},
"Date": nil,
"X-Content-Type-Options": {"nosniff"},
"X-Prpc-Grpc-Code": {strCode(codes.InvalidArgument)},
}))
assert.Loosely(t, res.Body.String(), should.Equal("the override check: BOOM\n"))
})
t.Run("Override callback: peek, pass through", func(t *ftt.Test) {
req.Header.Set("Accept", mtPRPCText)
var rpcReq *testpb.HelloRequest
server.RegisterOverride("prpc.Greeter", "SayHello",
func(rw http.ResponseWriter, req *http.Request, body func(msg proto.Message) error) (bool, error) {
r := &testpb.HelloRequest{}
if err := body(r); err != nil {
return false, err
}
rpcReq = r
return false, nil
},
)
r.ServeHTTP(res, req)
assert.Loosely(t, res.Code, should.Equal(http.StatusOK))
assert.Loosely(t, res.Result().Header, should.Resemble(http.Header{
"Content-Type": {"application/prpc; encoding=text"},
"Date": nil,
"X-Content-Type-Options": {"nosniff"},
"X-Prpc-Grpc-Code": {strCode(codes.OK)},
}))
assert.Loosely(t, decodeReply(res.Body.Bytes()), should.Resemble(&testpb.HelloReply{Message: "Hello Lucy"}))
assert.Loosely(t, rpcReq, should.Resemble(&testpb.HelloRequest{Name: "Lucy"}))
})
t.Run("Override callback: peek, override", func(t *ftt.Test) {
req.Header.Set("Accept", mtPRPCText)
var rpcReq *testpb.HelloRequest
var rawBody []byte
server.RegisterOverride("prpc.Greeter", "SayHello",
func(rw http.ResponseWriter, req *http.Request, body func(msg proto.Message) error) (bool, error) {
r := &testpb.HelloRequest{}
if err := body(r); err != nil {
return false, err
}
rpcReq = r
rw.Header().Set("Overridden", "1")
rawBody, _ = io.ReadAll(req.Body)
_, _ = fmt.Fprintf(rw, "Override")
return true, nil
},
)
r.ServeHTTP(res, req)
assert.Loosely(t, res.Code, should.Equal(http.StatusOK))
assert.Loosely(t, res.Result().Header, should.Resemble(http.Header{
"Content-Type": {"text/plain; charset=utf-8"},
"Overridden": {"1"},
}))
assert.Loosely(t, rpcReq, should.Resemble(&testpb.HelloRequest{Name: "Lucy"}))
assert.Loosely(t, res.Body.String(), should.Equal("Override"))
assert.Loosely(t, decodeRequest(rawBody), should.Resemble(&testpb.HelloRequest{Name: "Lucy"}))
})
t.Run("Override callback: malformed request, pass through", func(t *ftt.Test) {
req.Body = io.NopCloser(bytes.NewBufferString("not a proto"))
var callbackErr error
server.RegisterOverride("prpc.Greeter", "SayHello",
func(rw http.ResponseWriter, req *http.Request, body func(msg proto.Message) error) (bool, error) {
callbackErr = body(&testpb.HelloRequest{})
return false, nil // let the request be handled by the pRPC server
},
)
r.ServeHTTP(res, req)
assert.Loosely(t, callbackErr, should.ErrLike("could not decode body"))
assert.Loosely(t, res.Code, should.Equal(http.StatusBadRequest))
assert.Loosely(t, res.Result().Header, should.Resemble(http.Header{
"Content-Type": {"text/plain; charset=utf-8"},
"Date": nil,
"X-Content-Type-Options": {"nosniff"},
"X-Prpc-Grpc-Code": {strCode(codes.InvalidArgument)},
}))
assert.Loosely(t, res.Body.String(), should.HavePrefix("could not decode body"))
})
t.Run("Override callback: malformed request, override", func(t *ftt.Test) {
req.Body = io.NopCloser(bytes.NewBufferString("not a proto"))
var callbackErr error
var rawBody []byte
var rawBodyErr error
server.RegisterOverride("prpc.Greeter", "SayHello",
func(rw http.ResponseWriter, req *http.Request, body func(msg proto.Message) error) (bool, error) {
callbackErr = body(&testpb.HelloRequest{})
rawBody, rawBodyErr = io.ReadAll(req.Body)
rw.Header().Set("Overridden", "1")
_, _ = fmt.Fprintf(rw, "Override")
return true, nil
},
)
r.ServeHTTP(res, req)
assert.Loosely(t, callbackErr, should.ErrLike("could not decode body"))
assert.Loosely(t, string(rawBody), should.Equal("not a proto"))
assert.Loosely(t, rawBodyErr, should.BeNil)
assert.Loosely(t, res.Code, should.Equal(http.StatusOK))
assert.Loosely(t, res.Result().Header, should.Resemble(http.Header{
"Content-Type": {"text/plain; charset=utf-8"},
"Overridden": {"1"},
}))
assert.Loosely(t, res.Body.String(), should.Equal("Override"))
})
t.Run("Override callback: IO error when peeking, pass through", func(t *ftt.Test) {
req.Body = io.NopCloser(io.MultiReader(
bytes.NewBufferString(`name: "Zzz"`),
&erroringReader{errors.New("BOOM")},
))
var callbackErr error
server.RegisterOverride("prpc.Greeter", "SayHello",
func(rw http.ResponseWriter, req *http.Request, body func(msg proto.Message) error) (bool, error) {
callbackErr = body(&testpb.HelloRequest{})
return false, nil // let the request be handled by the pRPC server
},
)
r.ServeHTTP(res, req)
assert.Loosely(t, callbackErr, should.ErrLike("BOOM"))
assert.Loosely(t, res.Code, should.Equal(http.StatusBadRequest))
assert.Loosely(t, res.Result().Header, should.Resemble(http.Header{
"Content-Type": {"text/plain; charset=utf-8"},
"Date": nil,
"X-Content-Type-Options": {"nosniff"},
"X-Prpc-Grpc-Code": {strCode(codes.InvalidArgument)},
}))
assert.Loosely(t, res.Body.String(), should.HavePrefix("reading the request: BOOM"))
})
t.Run("Override callback: IO error when peeking, override", func(t *ftt.Test) {
req.Body = io.NopCloser(io.MultiReader(
bytes.NewBufferString(`name: "Zzz"`),
&erroringReader{errors.New("BOOM")},
))
var callbackErr error
var rawBody []byte
var rawBodyErr error
server.RegisterOverride("prpc.Greeter", "SayHello",
func(rw http.ResponseWriter, req *http.Request, body func(msg proto.Message) error) (bool, error) {
callbackErr = body(&testpb.HelloRequest{})
rawBody, rawBodyErr = io.ReadAll(req.Body)
rw.Header().Set("Overridden", "1")
_, _ = fmt.Fprintf(rw, "Override")
return true, nil
},
)
r.ServeHTTP(res, req)
assert.Loosely(t, callbackErr, should.ErrLike("BOOM"))
assert.Loosely(t, string(rawBody), should.Equal(`name: "Zzz"`))
assert.Loosely(t, rawBodyErr, should.ErrLike("BOOM"))
assert.Loosely(t, res.Code, should.Equal(http.StatusOK))
assert.Loosely(t, res.Result().Header, should.Resemble(http.Header{
"Content-Type": {"text/plain; charset=utf-8"},
"Overridden": {"1"},
}))
assert.Loosely(t, res.Body.String(), should.Equal("Override"))
})
t.Run("Override callback: request too big", func(t *ftt.Test) {
req.Body = io.NopCloser(bytes.NewReader(make([]byte, server.MaxRequestSize+1)))
var callbackErr error
server.RegisterOverride("prpc.Greeter", "SayHello",
func(rw http.ResponseWriter, req *http.Request, body func(msg proto.Message) error) (bool, error) {
callbackErr = body(&testpb.HelloRequest{})
return false, nil // let the request be handled by the pRPC server
},
)
r.ServeHTTP(res, req)
assert.Loosely(t, callbackErr, should.ErrLike("request body too large"))
assert.Loosely(t, res.Code, should.Equal(http.StatusServiceUnavailable))
assert.Loosely(t, res.Result().Header, should.Resemble(http.Header{
"Content-Type": {"text/plain; charset=utf-8"},
"Date": nil,
"X-Content-Type-Options": {"nosniff"},
"X-Prpc-Grpc-Code": {strCode(codes.Unavailable)},
}))
assert.Loosely(t, res.Body.String(), should.HavePrefix("reading the request: the request size exceeds the server limit"))
})
})
})
}
type erroringReader struct {
err error
}
func (r *erroringReader) Read(p []byte) (n int, err error) {
return 0, r.err
}