go.uber.org/yarpc@v1.72.1/transport/http/outbound_test.go (about)

     1  // Copyright (c) 2022 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package http
    22  
    23  import (
    24  	"bytes"
    25  	"context"
    26  	"crypto/tls"
    27  	"errors"
    28  	"io/ioutil"
    29  	"net/http"
    30  	"net/http/httptest"
    31  	"net/url"
    32  	"strconv"
    33  	"sync"
    34  	"testing"
    35  	"time"
    36  
    37  	"github.com/golang/mock/gomock"
    38  	"github.com/opentracing/opentracing-go"
    39  	"github.com/stretchr/testify/assert"
    40  	"github.com/stretchr/testify/require"
    41  	"go.uber.org/yarpc/api/peer/peertest"
    42  	"go.uber.org/yarpc/api/transport"
    43  	"go.uber.org/yarpc/encoding/raw"
    44  	"go.uber.org/yarpc/internal/testtime"
    45  	"go.uber.org/yarpc/peer/abstractpeer"
    46  	"go.uber.org/yarpc/pkg/lifecycle"
    47  	"go.uber.org/yarpc/yarpcerrors"
    48  )
    49  
    50  func TestNewOutbound(t *testing.T) {
    51  	ctrl := gomock.NewController(t)
    52  	chooser := peertest.NewMockChooser(ctrl)
    53  
    54  	out := NewOutbound(chooser)
    55  	require.NotNil(t, out)
    56  	assert.Equal(t, chooser, out.Chooser())
    57  }
    58  
    59  func TestTransportNamer(t *testing.T) {
    60  	assert.Equal(t, TransportName, NewOutbound(nil).TransportName())
    61  }
    62  
    63  func TestNewSingleOutboundPanic(t *testing.T) {
    64  	require.Panics(t, func() {
    65  		// invalid url should cause panic
    66  		NewTransport().NewSingleOutbound("127.0.0.1:")
    67  	},
    68  		"expected to panic")
    69  }
    70  
    71  func TestCreateRequest(t *testing.T) {
    72  	appHeader := map[string]string{
    73  		"app-key1": "app-val1",
    74  		"app-key2": "app-val2",
    75  	}
    76  	tests := []struct {
    77  		desc        string
    78  		urlTemplate *url.URL
    79  		treq        *transport.Request
    80  		wantError   bool
    81  	}{
    82  		{
    83  			desc:        "wrong uri",
    84  			urlTemplate: &url.URL{Scheme: "%"}, // invalid
    85  			treq:        &transport.Request{},
    86  			wantError:   true,
    87  		},
    88  		{
    89  			desc: "successful creation",
    90  			treq: &transport.Request{
    91  				Headers: transport.HeadersFromMap(appHeader),
    92  			},
    93  		},
    94  	}
    95  
    96  	for _, tt := range tests {
    97  		t.Run(tt.desc, func(t *testing.T) {
    98  			o := &Outbound{urlTemplate: defaultURLTemplate}
    99  			if tt.urlTemplate != nil {
   100  				o.urlTemplate = tt.urlTemplate
   101  			}
   102  			hreq, err := o.createRequest(tt.treq)
   103  			if tt.wantError {
   104  				assert.Error(t, err)
   105  				return
   106  			}
   107  			assert.Len(t, hreq.Header, len(appHeader), "wrong number of header")
   108  			for k, v := range appHeader {
   109  				assert.Equal(t, v, hreq.Header.Get(ApplicationHeaderPrefix+k), "header value mismatch")
   110  			}
   111  		})
   112  	}
   113  }
   114  
   115  func TestCallSuccess(t *testing.T) {
   116  	successServer := httptest.NewServer(http.HandlerFunc(
   117  		func(w http.ResponseWriter, req *http.Request) {
   118  			defer req.Body.Close()
   119  
   120  			ttl := req.Header.Get(TTLMSHeader)
   121  			ttlms, err := strconv.Atoi(ttl)
   122  			assert.NoError(t, err, "can parse TTL header")
   123  			assert.InDelta(t, ttlms, testtime.X*1000.0, testtime.X*5.0, "ttl header within tolerance")
   124  
   125  			assert.Equal(t, "caller", req.Header.Get(CallerHeader))
   126  			assert.Equal(t, "service", req.Header.Get(ServiceHeader))
   127  			assert.Equal(t, "raw", req.Header.Get(EncodingHeader))
   128  			assert.Equal(t, "hello", req.Header.Get(ProcedureHeader))
   129  
   130  			body, err := ioutil.ReadAll(req.Body)
   131  			if assert.NoError(t, err) {
   132  				assert.Equal(t, []byte("world"), body)
   133  			}
   134  
   135  			w.Header().Set("rpc-header-foo", "bar")
   136  			_, err = w.Write([]byte("great success"))
   137  			assert.NoError(t, err)
   138  		},
   139  	))
   140  	defer successServer.Close()
   141  
   142  	httpTransport := NewTransport()
   143  	defer httpTransport.Stop()
   144  	out := httpTransport.NewSingleOutbound(successServer.URL)
   145  	require.NoError(t, out.Start(), "failed to start outbound")
   146  	defer out.Stop()
   147  
   148  	ctx, cancel := context.WithTimeout(context.Background(), testtime.Second)
   149  	defer cancel()
   150  	res, err := out.Call(ctx, &transport.Request{
   151  		Caller:    "caller",
   152  		Service:   "service",
   153  		Encoding:  raw.Encoding,
   154  		Procedure: "hello",
   155  		Body:      bytes.NewReader([]byte("world")),
   156  	})
   157  	require.NoError(t, err)
   158  	defer res.Body.Close()
   159  
   160  	foo, ok := res.Headers.Get("foo")
   161  	assert.True(t, ok, "value for foo expected")
   162  	assert.Equal(t, "bar", foo, "foo value mismatch")
   163  
   164  	body, err := ioutil.ReadAll(res.Body)
   165  	if assert.NoError(t, err) {
   166  		assert.Equal(t, []byte("great success"), body)
   167  	}
   168  }
   169  
   170  func TestCallOneWaySuccessWithBody(t *testing.T) {
   171  	successServer := httptest.NewServer(http.HandlerFunc(
   172  		func(w http.ResponseWriter, req *http.Request) {
   173  			defer req.Body.Close()
   174  
   175  			ttl := req.Header.Get(TTLMSHeader)
   176  			ttlms, err := strconv.Atoi(ttl)
   177  			assert.NoError(t, err, "can parse TTL header")
   178  			assert.InDelta(t, ttlms, testtime.X*1000.0, testtime.X*5.0, "ttl header within tolerance")
   179  
   180  			assert.Equal(t, "caller", req.Header.Get(CallerHeader))
   181  			assert.Equal(t, "service", req.Header.Get(ServiceHeader))
   182  			assert.Equal(t, "raw", req.Header.Get(EncodingHeader))
   183  			assert.Equal(t, "hello", req.Header.Get(ProcedureHeader))
   184  
   185  			body, err := ioutil.ReadAll(req.Body)
   186  			if assert.NoError(t, err) {
   187  				assert.Equal(t, []byte("world"), body)
   188  			}
   189  
   190  			w.Header().Set("rpc-header-foo", "bar")
   191  			_, err = w.Write([]byte("great success"))
   192  			assert.NoError(t, err)
   193  		},
   194  	))
   195  	defer successServer.Close()
   196  
   197  	httpTransport := NewTransport()
   198  	defer httpTransport.Stop()
   199  	out := httpTransport.NewSingleOutbound(successServer.URL)
   200  	require.NoError(t, out.Start(), "failed to start outbound")
   201  	defer out.Stop()
   202  
   203  	ctx, cancel := context.WithTimeout(context.Background(), testtime.Second)
   204  	defer cancel()
   205  	ack, err := out.CallOneway(ctx, &transport.Request{
   206  		Caller:    "caller",
   207  		Service:   "service",
   208  		Encoding:  raw.Encoding,
   209  		Procedure: "hello",
   210  		Body:      bytes.NewReader([]byte("world")),
   211  	})
   212  	require.NoError(t, err)
   213  	require.NotNil(t, ack)
   214  }
   215  
   216  func TestCallOneWaySuccess(t *testing.T) {
   217  	successServer := httptest.NewServer(http.HandlerFunc(
   218  		func(w http.ResponseWriter, req *http.Request) {
   219  			defer req.Body.Close()
   220  
   221  			ttl := req.Header.Get(TTLMSHeader)
   222  			ttlms, err := strconv.Atoi(ttl)
   223  			assert.NoError(t, err, "can parse TTL header")
   224  			assert.InDelta(t, ttlms, testtime.X*1000.0, testtime.X*5.0, "ttl header within tolerance")
   225  
   226  			assert.Equal(t, "caller", req.Header.Get(CallerHeader))
   227  			assert.Equal(t, "service", req.Header.Get(ServiceHeader))
   228  			assert.Equal(t, "raw", req.Header.Get(EncodingHeader))
   229  			assert.Equal(t, "hello", req.Header.Get(ProcedureHeader))
   230  
   231  			body, err := ioutil.ReadAll(req.Body)
   232  			if assert.NoError(t, err) {
   233  				assert.Equal(t, []byte("world"), body)
   234  			}
   235  
   236  			w.Header().Set("rpc-header-foo", "bar")
   237  			assert.NoError(t, err)
   238  		},
   239  	))
   240  	defer successServer.Close()
   241  
   242  	httpTransport := NewTransport()
   243  	defer httpTransport.Stop()
   244  	out := httpTransport.NewSingleOutbound(successServer.URL)
   245  	require.NoError(t, out.Start(), "failed to start outbound")
   246  	defer out.Stop()
   247  
   248  	ctx, cancel := context.WithTimeout(context.Background(), testtime.Second)
   249  	defer cancel()
   250  	ack, err := out.CallOneway(ctx, &transport.Request{
   251  		Caller:    "caller",
   252  		Service:   "service",
   253  		Encoding:  raw.Encoding,
   254  		Procedure: "hello",
   255  		Body:      bytes.NewReader([]byte("world")),
   256  	})
   257  	require.NoError(t, err)
   258  	require.NotNil(t, ack)
   259  }
   260  
   261  func TestCallOneWayFailWithoutDeadline(t *testing.T) {
   262  	successServer := httptest.NewServer(nil)
   263  	defer successServer.Close()
   264  
   265  	httpTransport := NewTransport()
   266  	defer httpTransport.Stop()
   267  	out := httpTransport.NewSingleOutbound(successServer.URL)
   268  	require.NoError(t, out.Start(), "failed to start outbound")
   269  	defer out.Stop()
   270  
   271  	ack, err := out.CallOneway(context.Background(), &transport.Request{
   272  		Caller:    "caller",
   273  		Service:   "service",
   274  		Encoding:  raw.Encoding,
   275  		Procedure: "hello",
   276  		Body:      bytes.NewReader([]byte("world")),
   277  	})
   278  	require.Error(t, err)
   279  	require.Nil(t, ack)
   280  }
   281  
   282  func TestAddReservedHeader(t *testing.T) {
   283  	tests := []string{
   284  		"Rpc-Foo",
   285  		"rpc-header-foo",
   286  		"RPC-Bar",
   287  	}
   288  
   289  	for _, tt := range tests {
   290  		assert.Panics(t, func() { AddHeader(tt, "bar") })
   291  	}
   292  }
   293  
   294  func TestOutboundHeaders(t *testing.T) {
   295  	tests := []struct {
   296  		desc    string
   297  		context context.Context
   298  		headers transport.Headers
   299  		opts    []OutboundOption
   300  
   301  		wantHeaders map[string]string
   302  	}{
   303  		{
   304  			desc:    "application headers",
   305  			headers: transport.NewHeaders().With("foo", "bar").With("baz", "Qux"),
   306  			wantHeaders: map[string]string{
   307  				"Rpc-Header-Foo": "bar",
   308  				"Rpc-Header-Baz": "Qux",
   309  			},
   310  		},
   311  		{
   312  			desc:    "extra headers",
   313  			headers: transport.NewHeaders().With("x", "y"),
   314  			opts: []OutboundOption{
   315  				AddHeader("X-Foo", "bar"),
   316  				AddHeader("X-BAR", "BAZ"),
   317  			},
   318  			wantHeaders: map[string]string{
   319  				"Rpc-Header-X": "y",
   320  				"X-Foo":        "bar",
   321  				"X-Bar":        "BAZ",
   322  			},
   323  		},
   324  		{
   325  			desc:    "pseudo headers",
   326  			headers: transport.NewHeaders().With(":authority", "localhost").With(":path", "/my/path").With(":scheme", "http").With(":method", "POST").With("baz", "Qux"),
   327  			wantHeaders: map[string]string{
   328  				"Rpc-Header-Baz": "Qux",
   329  			},
   330  		},
   331  	}
   332  
   333  	httpTransport := NewTransport()
   334  	defer httpTransport.Stop()
   335  
   336  	for _, tt := range tests {
   337  		server := httptest.NewServer(http.HandlerFunc(
   338  			func(w http.ResponseWriter, r *http.Request) {
   339  				defer r.Body.Close()
   340  				for k, v := range tt.wantHeaders {
   341  					assert.Equal(
   342  						t, v, r.Header.Get(k), "%v: header %v did not match", tt.desc, k)
   343  				}
   344  			},
   345  		))
   346  		defer server.Close()
   347  
   348  		ctx := tt.context
   349  		if ctx == nil {
   350  			var cancel context.CancelFunc
   351  			ctx, cancel = context.WithTimeout(context.Background(), testtime.Second)
   352  			defer cancel()
   353  		}
   354  
   355  		out := httpTransport.NewSingleOutbound(server.URL, tt.opts...)
   356  		assert.Len(t, out.Transports(), 1, "transports must contain the transport")
   357  		// we use == instead of assert.Equal because we want to do a pointer
   358  		// comparison
   359  		assert.True(t, httpTransport == out.Transports()[0], "transports must match")
   360  
   361  		require.NoError(t, out.Start(), "failed to start outbound")
   362  		defer out.Stop()
   363  
   364  		res, err := out.Call(ctx, &transport.Request{
   365  			Caller:    "caller",
   366  			Service:   "service",
   367  			Encoding:  raw.Encoding,
   368  			Headers:   tt.headers,
   369  			Procedure: "hello",
   370  			Body:      bytes.NewReader([]byte("world")),
   371  		})
   372  
   373  		if !assert.NoError(t, err, "%v: call failed", tt.desc) {
   374  			continue
   375  		}
   376  
   377  		if !assert.NoError(t, res.Body.Close(), "%v: failed to close response body") {
   378  			continue
   379  		}
   380  	}
   381  }
   382  
   383  func TestOutboundApplicationError(t *testing.T) {
   384  	const (
   385  		appErrDetails = "thrift ex message"
   386  		appErrName    = "thrift ex name"
   387  	)
   388  
   389  	tests := []struct {
   390  		desc          string
   391  		status        string
   392  		appError      bool
   393  		appErrName    string
   394  		appErrDetails string
   395  		appErrCode    yarpcerrors.Code
   396  	}{
   397  		{
   398  			desc:     "ok",
   399  			status:   "success",
   400  			appError: false,
   401  		},
   402  		{
   403  			desc:          "error",
   404  			status:        "error",
   405  			appError:      true,
   406  			appErrName:    appErrName,
   407  			appErrDetails: appErrDetails,
   408  			appErrCode:    yarpcerrors.CodeNotFound,
   409  		},
   410  		{
   411  			desc:     "not an error",
   412  			status:   "lolwut",
   413  			appError: false,
   414  		},
   415  	}
   416  
   417  	httpTransport := NewTransport()
   418  	defer httpTransport.Stop()
   419  
   420  	for _, tt := range tests {
   421  		server := httptest.NewServer(http.HandlerFunc(
   422  			func(w http.ResponseWriter, r *http.Request) {
   423  				w.Header().Add("Rpc-Status", tt.status)
   424  
   425  				if tt.appError {
   426  					w.Header().Add(_applicationErrorDetailsHeader, tt.appErrDetails)
   427  					w.Header().Add(_applicationErrorNameHeader, tt.appErrName)
   428  					w.Header().Add(_applicationErrorCodeHeader, strconv.Itoa(int(tt.appErrCode)))
   429  				}
   430  
   431  				defer r.Body.Close()
   432  			},
   433  		))
   434  		defer server.Close()
   435  
   436  		out := httpTransport.NewSingleOutbound(server.URL)
   437  
   438  		require.NoError(t, out.Start(), "failed to start outbound")
   439  		defer out.Stop()
   440  
   441  		ctx := context.Background()
   442  		ctx, cancel := context.WithTimeout(ctx, 100*testtime.Millisecond)
   443  		defer cancel()
   444  
   445  		res, err := out.Call(ctx, &transport.Request{
   446  			Caller:    "caller",
   447  			Service:   "service",
   448  			Encoding:  raw.Encoding,
   449  			Procedure: "hello",
   450  			Body:      bytes.NewReader([]byte("world")),
   451  		})
   452  
   453  		assert.Equal(t, res.ApplicationError, tt.appError, "%v: application status", tt.desc)
   454  		if tt.appError {
   455  			require.NotNil(t, res.ApplicationErrorMeta)
   456  			assert.Equal(t, tt.appErrDetails, res.ApplicationErrorMeta.Details)
   457  			assert.Equal(t, tt.appErrName, res.ApplicationErrorMeta.Name)
   458  			assert.Equal(t, &tt.appErrCode, res.ApplicationErrorMeta.Code)
   459  		}
   460  
   461  		if !assert.NoError(t, err, "%v: call failed", tt.desc) {
   462  			continue
   463  		}
   464  
   465  		if !assert.NoError(t, res.Body.Close(), "%v: failed to close response body") {
   466  			continue
   467  		}
   468  	}
   469  }
   470  
   471  func TestCallFailures(t *testing.T) {
   472  	notFoundServer := httptest.NewServer(http.NotFoundHandler())
   473  	defer notFoundServer.Close()
   474  
   475  	internalErrorServer := httptest.NewServer(http.HandlerFunc(
   476  		func(w http.ResponseWriter, r *http.Request) {
   477  			http.Error(w, "great sadness", http.StatusInternalServerError)
   478  		}))
   479  	defer internalErrorServer.Close()
   480  
   481  	httpTransport := NewTransport()
   482  	defer httpTransport.Stop()
   483  
   484  	tests := []struct {
   485  		url      string
   486  		messages []string
   487  	}{
   488  		{"not a URL", []string{"protocol scheme"}},
   489  		{notFoundServer.URL, []string{"404", "page not found"}},
   490  		{internalErrorServer.URL, []string{"great sadness"}},
   491  	}
   492  
   493  	for _, tt := range tests {
   494  		out := httpTransport.NewSingleOutbound(tt.url)
   495  		require.NoError(t, out.Start(), "failed to start outbound")
   496  		defer out.Stop()
   497  
   498  		ctx, cancel := context.WithTimeout(context.Background(), testtime.Second)
   499  		defer cancel()
   500  		_, err := out.Call(ctx, &transport.Request{
   501  			Caller:    "caller",
   502  			Service:   "service",
   503  			Encoding:  raw.Encoding,
   504  			Procedure: "wat",
   505  			Body:      bytes.NewReader([]byte("huh")),
   506  		})
   507  		assert.Error(t, err, "expected failure")
   508  		for _, msg := range tt.messages {
   509  			assert.Contains(t, err.Error(), msg)
   510  		}
   511  	}
   512  }
   513  
   514  func TestStartMultiple(t *testing.T) {
   515  	httpTransport := NewTransport()
   516  	defer httpTransport.Stop()
   517  	out := httpTransport.NewSingleOutbound("http://localhost:9999")
   518  
   519  	var wg sync.WaitGroup
   520  	signal := make(chan struct{})
   521  
   522  	for i := 0; i < 10; i++ {
   523  		wg.Add(1)
   524  		go func() {
   525  			defer wg.Done()
   526  			<-signal
   527  
   528  			err := out.Start()
   529  			assert.NoError(t, err)
   530  		}()
   531  	}
   532  	close(signal)
   533  	wg.Wait()
   534  }
   535  
   536  func TestStopMultiple(t *testing.T) {
   537  	httpTransport := NewTransport()
   538  	defer httpTransport.Stop()
   539  	out := httpTransport.NewSingleOutbound("http://127.0.0.1:9999")
   540  
   541  	err := out.Start()
   542  	require.NoError(t, err)
   543  
   544  	var wg sync.WaitGroup
   545  	signal := make(chan struct{})
   546  
   547  	for i := 0; i < 10; i++ {
   548  		wg.Add(1)
   549  		go func() {
   550  			defer wg.Done()
   551  			<-signal
   552  
   553  			err := out.Stop()
   554  			assert.NoError(t, err)
   555  		}()
   556  	}
   557  	close(signal)
   558  	wg.Wait()
   559  }
   560  
   561  func TestCallWithoutStarting(t *testing.T) {
   562  	httpTransport := NewTransport()
   563  	defer httpTransport.Stop()
   564  	out := httpTransport.NewSingleOutbound("http://127.0.0.1:9999")
   565  
   566  	ctx, cancel := context.WithTimeout(context.Background(), 200*testtime.Millisecond)
   567  	defer cancel()
   568  	_, err := out.Call(
   569  		ctx,
   570  		&transport.Request{
   571  			Caller:    "caller",
   572  			Service:   "service",
   573  			Encoding:  raw.Encoding,
   574  			Procedure: "foo",
   575  			Body:      bytes.NewReader([]byte("sup")),
   576  		},
   577  	)
   578  
   579  	assert.Equal(t, yarpcerrors.FailedPreconditionErrorf("error waiting for HTTP outbound to start for service: service: context finished while waiting for instance to start: context deadline exceeded"), err)
   580  }
   581  
   582  func TestGetPeerForRequestErr(t *testing.T) {
   583  	ctrl := gomock.NewController(t)
   584  	defer ctrl.Finish()
   585  
   586  	tests := []struct {
   587  		name string
   588  		peer *peertest.MockPeer
   589  		err  error
   590  	}{
   591  		{
   592  			name: "error choosing peer",
   593  		},
   594  		{
   595  			name: "error casting peer",
   596  			peer: peertest.NewMockPeer(ctrl),
   597  			err:  errors.New("err"),
   598  		},
   599  	}
   600  
   601  	for _, tt := range tests {
   602  		t.Run(tt.name, func(t *testing.T) {
   603  			chooser := peertest.NewMockChooser(ctrl)
   604  
   605  			out := NewTransport().NewSingleOutbound("http://127.0.0.1:9999")
   606  			out.chooser = chooser
   607  
   608  			ctx := context.Background()
   609  			treq := &transport.Request{}
   610  
   611  			chooser.EXPECT().Choose(ctx, treq).Return(tt.peer, nil, tt.err)
   612  
   613  			_, _, err := out.getPeerForRequest(ctx, treq)
   614  			require.Error(t, err)
   615  		})
   616  	}
   617  }
   618  
   619  func TestWithCoreHeaders(t *testing.T) {
   620  	endpoint := "http://127.0.0.1:9999"
   621  	httpTransport := NewTransport()
   622  	defer httpTransport.Stop()
   623  	out := httpTransport.NewSingleOutbound(endpoint)
   624  	defer out.Stop()
   625  	require.NoError(t, out.Start())
   626  
   627  	httpReq := httptest.NewRequest("", endpoint, nil)
   628  
   629  	shardKey := "sharding"
   630  	routingKey := "routing"
   631  	routingDelegate := "delegate"
   632  	callerProcedure := "callerprocedure"
   633  
   634  	treq := &transport.Request{
   635  		ShardKey:        shardKey,
   636  		RoutingKey:      routingKey,
   637  		RoutingDelegate: routingDelegate,
   638  		CallerProcedure: callerProcedure,
   639  	}
   640  	result := out.withCoreHeaders(httpReq, treq, time.Second)
   641  
   642  	assert.Equal(t, shardKey, result.Header.Get(ShardKeyHeader))
   643  	assert.Equal(t, routingKey, result.Header.Get(RoutingKeyHeader))
   644  	assert.Equal(t, routingDelegate, result.Header.Get(RoutingDelegateHeader))
   645  	assert.Equal(t, callerProcedure, result.Header.Get(CallerProcedureHeader))
   646  }
   647  
   648  func TestNoRequest(t *testing.T) {
   649  	tran := NewTransport()
   650  	defer tran.Stop()
   651  	out := tran.NewSingleOutbound("localhost:0")
   652  
   653  	_, err := out.Call(context.Background(), nil)
   654  	assert.Equal(t, yarpcerrors.InvalidArgumentErrorf("request for http unary outbound was nil"), err)
   655  
   656  	_, err = out.CallOneway(context.Background(), nil)
   657  	assert.Equal(t, yarpcerrors.InvalidArgumentErrorf("request for http oneway outbound was nil"), err)
   658  }
   659  
   660  func TestOutboundNoDeadline(t *testing.T) {
   661  	tran := NewTransport()
   662  	defer tran.Stop()
   663  	out := tran.NewSingleOutbound("http://foo-host:8080")
   664  
   665  	_, err := out.call(context.Background(), &transport.Request{})
   666  	assert.Equal(t, yarpcerrors.Newf(yarpcerrors.CodeInvalidArgument, "missing context deadline"), err)
   667  }
   668  
   669  func TestServiceMatchSuccess(t *testing.T) {
   670  	matchServer := httptest.NewServer(http.HandlerFunc(
   671  		func(w http.ResponseWriter, req *http.Request) {
   672  			defer req.Body.Close()
   673  			w.Header().Set(ServiceHeader, req.Header.Get(ServiceHeader))
   674  			_, err := w.Write([]byte("Service name header return"))
   675  			assert.NoError(t, err)
   676  		},
   677  	))
   678  	defer matchServer.Close()
   679  
   680  	httpTransport := NewTransport()
   681  	defer httpTransport.Stop()
   682  	out := httpTransport.NewSingleOutbound(matchServer.URL)
   683  	require.NoError(t, out.Start(), "failed to start outbound")
   684  	defer out.Stop()
   685  
   686  	ctx, cancel := context.WithTimeout(context.Background(), testtime.Second)
   687  	defer cancel()
   688  	_, err := out.Call(ctx, &transport.Request{
   689  		Service: "Service",
   690  	})
   691  	require.NoError(t, err)
   692  }
   693  
   694  func TestServiceMatchFailed(t *testing.T) {
   695  	mismatchServer := httptest.NewServer(http.HandlerFunc(
   696  		func(w http.ResponseWriter, req *http.Request) {
   697  			defer req.Body.Close()
   698  			w.Header().Set(ServiceHeader, "ThisIsAWrongSvcName")
   699  			_, err := w.Write([]byte("Wrong service name header return"))
   700  			assert.NoError(t, err)
   701  		},
   702  	))
   703  	defer mismatchServer.Close()
   704  
   705  	httpTransport := NewTransport()
   706  	defer httpTransport.Stop()
   707  	out := httpTransport.NewSingleOutbound(mismatchServer.URL)
   708  	require.NoError(t, out.Start(), "failed to start outbound")
   709  	defer out.Stop()
   710  
   711  	ctx, cancel := context.WithTimeout(context.Background(), testtime.Second)
   712  	defer cancel()
   713  	_, err := out.Call(ctx, &transport.Request{
   714  		Service: "Service",
   715  	})
   716  	assert.Error(t, err, "expected failure for service name dismatch")
   717  }
   718  
   719  func TestServiceMatchNoHeader(t *testing.T) {
   720  	noHeaderServer := httptest.NewServer(http.HandlerFunc(
   721  		func(w http.ResponseWriter, req *http.Request) {
   722  			defer req.Body.Close()
   723  			// intentionally do not set a response header
   724  			_, err := w.Write([]byte("No service name header return"))
   725  			assert.NoError(t, err)
   726  		},
   727  	))
   728  	defer noHeaderServer.Close()
   729  
   730  	httpTransport := NewTransport()
   731  	defer httpTransport.Stop()
   732  	out := httpTransport.NewSingleOutbound(noHeaderServer.URL)
   733  	require.NoError(t, out.Start(), "failed to start outbound")
   734  	defer out.Stop()
   735  
   736  	ctx, cancel := context.WithTimeout(context.Background(), testtime.Second)
   737  	defer cancel()
   738  	_, err := out.Call(ctx, &transport.Request{
   739  		Service: "Service",
   740  	})
   741  	require.NoError(t, err)
   742  }
   743  
   744  type RoundTripFunc func(req *http.Request) *http.Response
   745  
   746  func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
   747  	return f(req), nil
   748  }
   749  
   750  type errorReadCloser struct {
   751  	closeErr error
   752  }
   753  
   754  func (errorReadCloser) Read(p []byte) (n int, err error) {
   755  	return
   756  }
   757  
   758  func (e errorReadCloser) Close() error {
   759  	return e.closeErr
   760  }
   761  
   762  func TestCallResponseCloseError(t *testing.T) {
   763  	httpTransport := Transport{
   764  		client: &http.Client{
   765  			Transport: RoundTripFunc(func(req *http.Request) *http.Response {
   766  				return &http.Response{
   767  					StatusCode: 200,
   768  					Body:       errorReadCloser{closeErr: errors.New("test error")},
   769  					Header: http.Header{
   770  						"Rpc-Service": []string{"wrong-service"},
   771  					},
   772  				}
   773  			}),
   774  		},
   775  		tracer: opentracing.GlobalTracer(),
   776  	}
   777  	ctrl := gomock.NewController(t)
   778  	chooser := peertest.NewMockChooser(ctrl)
   779  	chooser.EXPECT().Start().Return(nil)
   780  	peer := &httpPeer{
   781  		Peer: &abstractpeer.Peer{},
   782  	}
   783  	chooser.EXPECT().Choose(gomock.Any(), gomock.Any()).Return(peer, func(error) {}, nil)
   784  	o := &Outbound{
   785  		once:              lifecycle.NewOnce(),
   786  		chooser:           chooser,
   787  		urlTemplate:       defaultURLTemplate,
   788  		tracer:            httpTransport.tracer,
   789  		transport:         &httpTransport,
   790  		bothResponseError: true,
   791  		client:            httpTransport.client,
   792  	}
   793  	err := o.Start()
   794  	require.NoError(t, err)
   795  	ctx, cancel := context.WithTimeout(context.Background(), testtime.Second)
   796  	defer cancel()
   797  	_, err = o.Call(ctx, &transport.Request{
   798  		Service: "Service",
   799  	})
   800  	require.Errorf(t, err, "Received unexpected error:code:internal message:test error")
   801  }
   802  
   803  func TestCallOneWayResponseCloseError(t *testing.T) {
   804  	httpTransport := Transport{
   805  		client: &http.Client{
   806  			Transport: RoundTripFunc(func(req *http.Request) *http.Response {
   807  				return &http.Response{
   808  					StatusCode: 200,
   809  					Body:       errorReadCloser{closeErr: errors.New("test error")},
   810  					Header:     http.Header{},
   811  				}
   812  			}),
   813  		},
   814  		tracer: opentracing.GlobalTracer(),
   815  	}
   816  	ctrl := gomock.NewController(t)
   817  	chooser := peertest.NewMockChooser(ctrl)
   818  	chooser.EXPECT().Start().Return(nil)
   819  	peer := &httpPeer{
   820  		Peer: &abstractpeer.Peer{},
   821  	}
   822  	chooser.EXPECT().Choose(gomock.Any(), gomock.Any()).Return(peer, func(error) {}, nil)
   823  	o := &Outbound{
   824  		once:              lifecycle.NewOnce(),
   825  		chooser:           chooser,
   826  		urlTemplate:       defaultURLTemplate,
   827  		tracer:            httpTransport.tracer,
   828  		transport:         &httpTransport,
   829  		bothResponseError: true,
   830  		client:            httpTransport.client,
   831  	}
   832  	err := o.Start()
   833  	require.NoError(t, err)
   834  	ctx, cancel := context.WithTimeout(context.Background(), testtime.Second)
   835  	defer cancel()
   836  	_, err = o.CallOneway(ctx, &transport.Request{
   837  		Service: "Service",
   838  	})
   839  	require.Errorf(t, err, "Received unexpected error:code:internal message:test error")
   840  }
   841  
   842  func TestIsolatedSchemaChange(t *testing.T) {
   843  	tr := &Transport{client: &http.Client{Transport: http.DefaultTransport}}
   844  	plainOutbound := tr.NewOutbound(nil)
   845  	tlsOutbound := tr.NewOutbound(nil, OutboundTLSConfiguration(&tls.Config{}))
   846  	assert.NotEqual(t, plainOutbound.urlTemplate, tlsOutbound.urlTemplate)
   847  	assert.Equal(t, "http", plainOutbound.urlTemplate.Scheme)
   848  	assert.Equal(t, "https", tlsOutbound.urlTemplate.Scheme)
   849  }