go.uber.org/yarpc@v1.72.1/transport/http/outbound.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  	"context"
    25  	"crypto/tls"
    26  	"fmt"
    27  	"io/ioutil"
    28  	"log"
    29  	"net/http"
    30  	"net/url"
    31  	"strconv"
    32  	"strings"
    33  	"time"
    34  
    35  	"github.com/opentracing/opentracing-go"
    36  	"github.com/opentracing/opentracing-go/ext"
    37  	opentracinglog "github.com/opentracing/opentracing-go/log"
    38  	"go.uber.org/yarpc"
    39  	"go.uber.org/yarpc/api/peer"
    40  	"go.uber.org/yarpc/api/transport"
    41  	"go.uber.org/yarpc/api/x/introspection"
    42  	intyarpcerrors "go.uber.org/yarpc/internal/yarpcerrors"
    43  	peerchooser "go.uber.org/yarpc/peer"
    44  	"go.uber.org/yarpc/peer/hostport"
    45  	"go.uber.org/yarpc/pkg/lifecycle"
    46  	"go.uber.org/yarpc/transport/internal/tls/dialer"
    47  	"go.uber.org/yarpc/yarpcerrors"
    48  )
    49  
    50  // this ensures the HTTP outbound implements both transport.Outbound interfaces
    51  var (
    52  	_ transport.Namer                      = (*Outbound)(nil)
    53  	_ transport.UnaryOutbound              = (*Outbound)(nil)
    54  	_ transport.OnewayOutbound             = (*Outbound)(nil)
    55  	_ introspection.IntrospectableOutbound = (*Outbound)(nil)
    56  )
    57  
    58  var defaultURLTemplate, _ = url.Parse("http://localhost")
    59  
    60  // OutboundOption customizes an HTTP Outbound.
    61  type OutboundOption func(*Outbound)
    62  
    63  func (OutboundOption) httpOption() {}
    64  
    65  // URLTemplate specifies the URL this outbound makes requests to. For
    66  // peer.Chooser-based outbounds, the peer (host:port) spection of the URL may
    67  // vary from call to call but the rest will remain unchanged. For single-peer
    68  // outbounds, the URL will be used as-is.
    69  func URLTemplate(template string) OutboundOption {
    70  	return func(o *Outbound) {
    71  		o.setURLTemplate(template)
    72  	}
    73  }
    74  
    75  // AddHeader specifies that an HTTP outbound should always include the given
    76  // header in outgoung requests.
    77  //
    78  //	httpTransport.NewOutbound(chooser, http.AddHeader("X-Token", "TOKEN"))
    79  //
    80  // Note that headers starting with "Rpc-" are reserved by YARPC. This function
    81  // will panic if the header starts with "Rpc-".
    82  func AddHeader(key, value string) OutboundOption {
    83  	if strings.HasPrefix(strings.ToLower(key), "rpc-") {
    84  		panic(fmt.Errorf(
    85  			"invalid header name %q: "+
    86  				`headers starting with "Rpc-" are reserved by YARPC`, key))
    87  	}
    88  
    89  	return func(o *Outbound) {
    90  		if o.headers == nil {
    91  			o.headers = make(http.Header)
    92  		}
    93  		o.headers.Add(key, value)
    94  	}
    95  }
    96  
    97  // OutboundTLSConfiguration return a OutboundOption which provides tls config
    98  // for the outbound.
    99  func OutboundTLSConfiguration(config *tls.Config) OutboundOption {
   100  	return func(o *Outbound) {
   101  		o.tlsConfig = config
   102  	}
   103  }
   104  
   105  // OutboundDestinationServiceName returns a OutboundOption which provides the
   106  // name of the destination service. Mostly used in outbound TLS dialer metrics.
   107  func OutboundDestinationServiceName(name string) OutboundOption {
   108  	return func(o *Outbound) {
   109  		o.destServiceName = name
   110  	}
   111  }
   112  
   113  // NewOutbound builds an HTTP outbound that sends requests to peers supplied
   114  // by the given peer.Chooser. The URL template for used for the different
   115  // peers may be customized using the URLTemplate option.
   116  //
   117  // The peer chooser and outbound must share the same transport, in this case
   118  // the HTTP transport.
   119  // The peer chooser must use the transport's RetainPeer to obtain peer
   120  // instances and return those peers to the outbound when it calls Choose.
   121  // The concrete peer type is private and intrinsic to the HTTP transport.
   122  func (t *Transport) NewOutbound(chooser peer.Chooser, opts ...OutboundOption) *Outbound {
   123  	o := &Outbound{
   124  		once:              lifecycle.NewOnce(),
   125  		chooser:           chooser,
   126  		urlTemplate:       defaultURLTemplate,
   127  		tracer:            t.tracer,
   128  		transport:         t,
   129  		bothResponseError: true,
   130  	}
   131  	for _, opt := range opts {
   132  		opt(o)
   133  	}
   134  
   135  	client := t.client
   136  	if o.tlsConfig != nil {
   137  		client = createTLSClient(o)
   138  		// Create a copy of the url template to avoid scheme changes impacting
   139  		// other outbounds as the base url template is shared across http
   140  		// outbounds.
   141  		ut := *o.urlTemplate
   142  		ut.Scheme = "https"
   143  		o.urlTemplate = &ut
   144  	}
   145  	o.client = client
   146  	o.sender = &transportSender{Client: client}
   147  	return o
   148  }
   149  
   150  func createTLSClient(o *Outbound) *http.Client {
   151  	transport, ok := o.transport.client.Transport.(*http.Transport)
   152  	if !ok {
   153  		// This should not happen as default yarpc http.Client uses
   154  		// http.Transport and it's not configurable by the user.
   155  		panic(fmt.Sprintf("failed to create http tls client, provided http.Client transport type %T is not *http.Transport", o.transport.client.Transport))
   156  	}
   157  
   158  	tlsDialer := dialer.NewTLSDialer(dialer.Params{
   159  		Config:        o.tlsConfig,
   160  		Meter:         o.transport.meter,
   161  		Logger:        o.transport.logger,
   162  		ServiceName:   o.transport.serviceName,
   163  		TransportName: TransportName,
   164  		Dest:          o.destServiceName,
   165  		Dialer:        transport.DialContext,
   166  	})
   167  	transport = transport.Clone()
   168  	transport.DialTLSContext = tlsDialer.DialContext
   169  	return &http.Client{Transport: transport}
   170  }
   171  
   172  // NewOutbound builds an HTTP outbound that sends requests to peers supplied
   173  // by the given peer.Chooser. The URL template for used for the different
   174  // peers may be customized using the URLTemplate option.
   175  //
   176  // The peer chooser and outbound must share the same transport, in this case
   177  // the HTTP transport.
   178  // The peer chooser must use the transport's RetainPeer to obtain peer
   179  // instances and return those peers to the outbound when it calls Choose.
   180  // The concrete peer type is private and intrinsic to the HTTP transport.
   181  func NewOutbound(chooser peer.Chooser, opts ...OutboundOption) *Outbound {
   182  	return NewTransport().NewOutbound(chooser, opts...)
   183  }
   184  
   185  // NewSingleOutbound builds an outbound that sends YARPC requests over HTTP
   186  // to the specified URL.
   187  //
   188  // The URLTemplate option has no effect in this form.
   189  func (t *Transport) NewSingleOutbound(uri string, opts ...OutboundOption) *Outbound {
   190  	parsedURL, err := url.Parse(uri)
   191  	if err != nil {
   192  		panic(err.Error())
   193  	}
   194  
   195  	chooser := peerchooser.NewSingle(hostport.PeerIdentifier(parsedURL.Host), t)
   196  	opts = append(opts, URLTemplate(uri))
   197  	return t.NewOutbound(chooser, opts...)
   198  }
   199  
   200  // Outbound sends YARPC requests over HTTP. It may be constructed using the
   201  // NewOutbound function or the NewOutbound or NewSingleOutbound methods on the
   202  // HTTP Transport. It is recommended that services use a single HTTP transport
   203  // to construct all HTTP outbounds, ensuring efficient sharing of resources
   204  // across the different outbounds.
   205  type Outbound struct {
   206  	chooser     peer.Chooser
   207  	urlTemplate *url.URL
   208  	tracer      opentracing.Tracer
   209  	transport   *Transport
   210  	sender      sender
   211  
   212  	// Headers to add to all outgoing requests.
   213  	headers http.Header
   214  
   215  	once *lifecycle.Once
   216  
   217  	// should only be false in testing
   218  	bothResponseError bool
   219  	destServiceName   string
   220  	client            *http.Client
   221  	tlsConfig         *tls.Config
   222  }
   223  
   224  // TransportName is the transport name that will be set on `transport.Request` struct.
   225  func (o *Outbound) TransportName() string {
   226  	return TransportName
   227  }
   228  
   229  // setURLTemplate configures an alternate URL template.
   230  // The host:port portion of the URL template gets replaced by the chosen peer's
   231  // identifier for each outbound request.
   232  func (o *Outbound) setURLTemplate(URL string) {
   233  	parsedURL, err := url.Parse(URL)
   234  	if err != nil {
   235  		log.Fatalf("failed to configure HTTP outbound: invalid URL template %q: %s", URL, err)
   236  	}
   237  	o.urlTemplate = parsedURL
   238  }
   239  
   240  // Transports returns the outbound's HTTP transport.
   241  func (o *Outbound) Transports() []transport.Transport {
   242  	return []transport.Transport{o.transport}
   243  }
   244  
   245  // Chooser returns the outbound's peer chooser.
   246  func (o *Outbound) Chooser() peer.Chooser {
   247  	return o.chooser
   248  }
   249  
   250  // Start the HTTP outbound
   251  func (o *Outbound) Start() error {
   252  	return o.once.Start(o.chooser.Start)
   253  }
   254  
   255  // Stop the HTTP outbound
   256  func (o *Outbound) Stop() error {
   257  	return o.once.Stop(o.chooser.Stop)
   258  }
   259  
   260  // IsRunning returns whether the Outbound is running.
   261  func (o *Outbound) IsRunning() bool {
   262  	return o.once.IsRunning()
   263  }
   264  
   265  // Call makes a HTTP request
   266  func (o *Outbound) Call(ctx context.Context, treq *transport.Request) (*transport.Response, error) {
   267  	if treq == nil {
   268  		return nil, yarpcerrors.InvalidArgumentErrorf("request for http unary outbound was nil")
   269  	}
   270  
   271  	return o.call(ctx, treq)
   272  }
   273  
   274  // CallOneway makes a oneway request
   275  func (o *Outbound) CallOneway(ctx context.Context, treq *transport.Request) (transport.Ack, error) {
   276  	if treq == nil {
   277  		return nil, yarpcerrors.InvalidArgumentErrorf("request for http oneway outbound was nil")
   278  	}
   279  
   280  	// res is used to close the response body to avoid memory/connection leak
   281  	// even when the response body is empty
   282  	res, err := o.call(ctx, treq)
   283  	if err != nil {
   284  		return nil, err
   285  	}
   286  
   287  	if err = res.Body.Close(); err != nil {
   288  		return nil, yarpcerrors.Newf(yarpcerrors.CodeInternal, err.Error())
   289  	}
   290  
   291  	return time.Now(), nil
   292  }
   293  
   294  func (o *Outbound) call(ctx context.Context, treq *transport.Request) (*transport.Response, error) {
   295  	start := time.Now()
   296  	deadline, ok := ctx.Deadline()
   297  	if !ok {
   298  		return nil, yarpcerrors.Newf(yarpcerrors.CodeInvalidArgument, "missing context deadline")
   299  	}
   300  	ttl := deadline.Sub(start)
   301  
   302  	hreq, err := o.createRequest(treq)
   303  	if err != nil {
   304  		return nil, err
   305  	}
   306  	ctx, hreq, span, err := o.withOpentracingSpan(ctx, hreq, treq, start)
   307  	if err != nil {
   308  		return nil, err
   309  	}
   310  	defer span.Finish()
   311  
   312  	hreq = o.withCoreHeaders(hreq, treq, ttl)
   313  	hreq = hreq.WithContext(ctx)
   314  
   315  	response, err := o.roundTrip(hreq, treq, start, o.client)
   316  	if err != nil {
   317  		span.SetTag("error", true)
   318  		span.LogFields(opentracinglog.String("event", err.Error()))
   319  		return nil, err
   320  	}
   321  
   322  	span.SetTag("http.status_code", response.StatusCode)
   323  
   324  	// Service name match validation, return yarpcerrors.CodeInternal error if not match
   325  	if match, resSvcName := checkServiceMatch(treq.Service, response.Header); !match {
   326  		if err = response.Body.Close(); err != nil {
   327  			return nil, yarpcerrors.Newf(yarpcerrors.CodeInternal, err.Error())
   328  		}
   329  		return nil, transport.UpdateSpanWithErr(span,
   330  			yarpcerrors.InternalErrorf("service name sent from the request "+
   331  				"does not match the service name received in the response, sent %q, got: %q", treq.Service, resSvcName))
   332  	}
   333  
   334  	tres := &transport.Response{
   335  		Headers:          applicationHeaders.FromHTTPHeaders(response.Header, transport.NewHeaders()),
   336  		Body:             response.Body,
   337  		BodySize:         int(response.ContentLength),
   338  		ApplicationError: response.Header.Get(ApplicationStatusHeader) == ApplicationErrorStatus,
   339  		ApplicationErrorMeta: &transport.ApplicationErrorMeta{
   340  			Details: response.Header.Get(_applicationErrorDetailsHeader),
   341  			Name:    response.Header.Get(_applicationErrorNameHeader),
   342  			Code:    getYARPCApplicationErrorCode(response.Header.Get(_applicationErrorCodeHeader)),
   343  		},
   344  	}
   345  
   346  	bothResponseError := response.Header.Get(BothResponseErrorHeader) == AcceptTrue
   347  	if bothResponseError && o.bothResponseError {
   348  		if response.StatusCode >= 300 {
   349  			return getYARPCErrorFromResponse(tres, response, true)
   350  		}
   351  		return tres, nil
   352  	}
   353  	if response.StatusCode >= 200 && response.StatusCode < 300 {
   354  		return tres, nil
   355  	}
   356  	return getYARPCErrorFromResponse(tres, response, false)
   357  }
   358  
   359  func getYARPCApplicationErrorCode(code string) *yarpcerrors.Code {
   360  	if code == "" {
   361  		return nil
   362  	}
   363  
   364  	errorCode, err := strconv.Atoi(code)
   365  	if err != nil {
   366  		return nil
   367  	}
   368  
   369  	yarpcCode := yarpcerrors.Code(errorCode)
   370  	return &yarpcCode
   371  }
   372  
   373  func (o *Outbound) getPeerForRequest(ctx context.Context, treq *transport.Request) (*httpPeer, func(error), error) {
   374  	p, onFinish, err := o.chooser.Choose(ctx, treq)
   375  	if err != nil {
   376  		return nil, nil, err
   377  	}
   378  
   379  	hpPeer, ok := p.(*httpPeer)
   380  	if !ok {
   381  		return nil, nil, peer.ErrInvalidPeerConversion{
   382  			Peer:         p,
   383  			ExpectedType: "*httpPeer",
   384  		}
   385  	}
   386  
   387  	return hpPeer, onFinish, nil
   388  }
   389  
   390  func (o *Outbound) createRequest(treq *transport.Request) (*http.Request, error) {
   391  	newURL := *o.urlTemplate
   392  	hreq, err := http.NewRequest("POST", newURL.String(), treq.Body)
   393  	if err != nil {
   394  		return nil, err
   395  	}
   396  	// YARPC needs to remove all the HTTP/2 pseudo headers when a HTTP/2 request (gRPC)
   397  	// was propagated from a YARPC transport middleware to a HTTP/1 service.
   398  	// It should be noted that net/http will return an error if a pseudo
   399  	// header is given along a HTTP/1 request.
   400  	// see: https://cs.opensource.google/go/x/net/+/c6fcb2db:http/httpguts/httplex.go;l=203
   401  	headers := applicationHeaders.deleteHTTP2PseudoHeadersIfNeeded(treq.Headers)
   402  	hreq.Header = applicationHeaders.ToHTTPHeaders(headers, nil)
   403  	return hreq, nil
   404  }
   405  
   406  func (o *Outbound) withOpentracingSpan(ctx context.Context, req *http.Request, treq *transport.Request, start time.Time) (context.Context, *http.Request, opentracing.Span, error) {
   407  	// Apply HTTP Context headers for tracing and baggage carried by tracing.
   408  	tracer := o.tracer
   409  	var parent opentracing.SpanContext // ok to be nil
   410  	if parentSpan := opentracing.SpanFromContext(ctx); parentSpan != nil {
   411  		parent = parentSpan.Context()
   412  	}
   413  	tags := opentracing.Tags{
   414  		"rpc.caller":    treq.Caller,
   415  		"rpc.service":   treq.Service,
   416  		"rpc.encoding":  treq.Encoding,
   417  		"rpc.transport": "http",
   418  	}
   419  	for k, v := range yarpc.OpentracingTags {
   420  		tags[k] = v
   421  	}
   422  	span := tracer.StartSpan(
   423  		treq.Procedure,
   424  		opentracing.StartTime(start),
   425  		opentracing.ChildOf(parent),
   426  		tags,
   427  	)
   428  	ext.PeerService.Set(span, treq.Service)
   429  	ext.SpanKindRPCClient.Set(span)
   430  	ext.HTTPUrl.Set(span, req.URL.String())
   431  	ctx = opentracing.ContextWithSpan(ctx, span)
   432  
   433  	err := tracer.Inject(
   434  		span.Context(),
   435  		opentracing.HTTPHeaders,
   436  		opentracing.HTTPHeadersCarrier(req.Header),
   437  	)
   438  
   439  	return ctx, req, span, err
   440  }
   441  
   442  func (o *Outbound) withCoreHeaders(req *http.Request, treq *transport.Request, ttl time.Duration) *http.Request {
   443  	// Add default headers to all requests.
   444  	for k, vs := range o.headers {
   445  		for _, v := range vs {
   446  			req.Header.Add(k, v)
   447  		}
   448  	}
   449  
   450  	req.Header.Set(CallerHeader, treq.Caller)
   451  	req.Header.Set(ServiceHeader, treq.Service)
   452  	req.Header.Set(ProcedureHeader, treq.Procedure)
   453  	if ttl != 0 {
   454  		req.Header.Set(TTLMSHeader, fmt.Sprintf("%d", ttl/time.Millisecond))
   455  	}
   456  	if treq.ShardKey != "" {
   457  		req.Header.Set(ShardKeyHeader, treq.ShardKey)
   458  	}
   459  	if treq.RoutingKey != "" {
   460  		req.Header.Set(RoutingKeyHeader, treq.RoutingKey)
   461  	}
   462  	if treq.RoutingDelegate != "" {
   463  		req.Header.Set(RoutingDelegateHeader, treq.RoutingDelegate)
   464  	}
   465  	if treq.CallerProcedure != "" {
   466  		req.Header.Set(CallerProcedureHeader, treq.CallerProcedure)
   467  	}
   468  
   469  	encoding := string(treq.Encoding)
   470  	if encoding != "" {
   471  		req.Header.Set(EncodingHeader, encoding)
   472  	}
   473  
   474  	if o.bothResponseError {
   475  		req.Header.Set(AcceptsBothResponseErrorHeader, AcceptTrue)
   476  	}
   477  
   478  	return req
   479  }
   480  
   481  func getYARPCErrorFromResponse(tres *transport.Response, response *http.Response, bothResponseError bool) (*transport.Response, error) {
   482  	var contents string
   483  	var details []byte
   484  	if bothResponseError {
   485  		contents = response.Header.Get(ErrorMessageHeader)
   486  		if response.Header.Get(ErrorDetailsHeader) != "" {
   487  			// the contents of this header and the body should be the same, but
   488  			// use the contents in the body, in case the contents were not ASCII and
   489  			// the contents were not preserved in the header.
   490  			var err error
   491  			details, err = ioutil.ReadAll(response.Body)
   492  			if err != nil {
   493  				return tres, yarpcerrors.Newf(yarpcerrors.CodeInternal, err.Error())
   494  			}
   495  			if err := response.Body.Close(); err != nil {
   496  				return tres, yarpcerrors.Newf(yarpcerrors.CodeInternal, err.Error())
   497  			}
   498  			// nil out body so that it isn't read later
   499  			tres.Body = nil
   500  		}
   501  	} else {
   502  		contentsBytes, err := ioutil.ReadAll(response.Body)
   503  		if err != nil {
   504  			return nil, yarpcerrors.Newf(yarpcerrors.CodeInternal, err.Error())
   505  		}
   506  		contents = string(contentsBytes)
   507  		if err := response.Body.Close(); err != nil {
   508  			return nil, yarpcerrors.Newf(yarpcerrors.CodeInternal, err.Error())
   509  		}
   510  	}
   511  	// use the status code if we can't get a code from the headers
   512  	code := statusCodeToBestCode(response.StatusCode)
   513  	if errorCodeText := response.Header.Get(ErrorCodeHeader); errorCodeText != "" {
   514  		var errorCode yarpcerrors.Code
   515  		// TODO: what to do with error?
   516  		if err := errorCode.UnmarshalText([]byte(errorCodeText)); err == nil {
   517  			code = errorCode
   518  		}
   519  	}
   520  	yarpcErr := intyarpcerrors.NewWithNamef(
   521  		code,
   522  		response.Header.Get(ErrorNameHeader),
   523  		strings.TrimSuffix(contents, "\n"),
   524  	).WithDetails(details)
   525  
   526  	if bothResponseError {
   527  		return tres, yarpcErr
   528  	}
   529  	return nil, yarpcErr
   530  }
   531  
   532  // Only does verification if there is a response header
   533  func checkServiceMatch(reqSvcName string, resHeaders http.Header) (bool, string) {
   534  	serviceName := resHeaders.Get(ServiceHeader)
   535  	return serviceName == "" || serviceName == reqSvcName, serviceName
   536  }
   537  
   538  // RoundTrip implements the http.RoundTripper interface, making a YARPC HTTP outbound suitable as a
   539  // Transport when constructing an HTTP Client. An HTTP client is suitable only for relative paths to
   540  // a single outbound service. The HTTP outbound overrides the host:port portion of the URL of the
   541  // provided request.
   542  //
   543  // Sample usage:
   544  //
   545  //	client := http.Client{Transport: outbound}
   546  //
   547  // Thereafter use the Golang standard library HTTP to send requests with this client.
   548  //
   549  //	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
   550  //	defer cancel()
   551  //	req, err := http.NewRequest("GET", "http://example.com/", nil /* body */)
   552  //	req = req.WithContext(ctx)
   553  //	res, err := client.Do(req)
   554  //
   555  // All requests must have a deadline on the context.
   556  // The peer chooser for raw HTTP requests will receive a YARPC transport.Request with no body.
   557  //
   558  // OpenTracing information must be added manually, before this call, to support context propagation.
   559  func (o *Outbound) RoundTrip(hreq *http.Request) (*http.Response, error) {
   560  	return o.roundTrip(hreq, nil /* treq */, time.Now(), o.sender)
   561  }
   562  
   563  func (o *Outbound) roundTrip(hreq *http.Request, treq *transport.Request, start time.Time, sender sender) (*http.Response, error) {
   564  	ctx := hreq.Context()
   565  
   566  	deadline, ok := ctx.Deadline()
   567  	if !ok {
   568  		return nil, yarpcerrors.Newf(
   569  			yarpcerrors.CodeInvalidArgument,
   570  			"missing context deadline")
   571  	}
   572  	ttl := deadline.Sub(start)
   573  
   574  	// When sending requests through the RoundTrip method, we construct the
   575  	// transport request from the HTTP headers as if it were an inbound
   576  	// request.
   577  	// The API for setting transport metadata for an outbound request when
   578  	// using the go stdlib HTTP client is to use headers as the YAPRC HTTP
   579  	// transport header conventions.
   580  	if treq == nil {
   581  		treq = &transport.Request{
   582  			Caller:          hreq.Header.Get(CallerHeader),
   583  			Service:         hreq.Header.Get(ServiceHeader),
   584  			Encoding:        transport.Encoding(hreq.Header.Get(EncodingHeader)),
   585  			Procedure:       hreq.Header.Get(ProcedureHeader),
   586  			ShardKey:        hreq.Header.Get(ShardKeyHeader),
   587  			RoutingKey:      hreq.Header.Get(RoutingKeyHeader),
   588  			RoutingDelegate: hreq.Header.Get(RoutingDelegateHeader),
   589  			CallerProcedure: hreq.Header.Get(CallerProcedureHeader),
   590  			Headers:         applicationHeaders.FromHTTPHeaders(hreq.Header, transport.Headers{}),
   591  		}
   592  	}
   593  
   594  	if err := o.once.WaitUntilRunning(ctx); err != nil {
   595  		return nil, intyarpcerrors.AnnotateWithInfo(
   596  			yarpcerrors.FromError(err),
   597  			"error waiting for HTTP outbound to start for service: %s",
   598  			treq.Service)
   599  	}
   600  
   601  	p, onFinish, err := o.getPeerForRequest(ctx, treq)
   602  	if err != nil {
   603  		return nil, err
   604  	}
   605  
   606  	hres, err := o.doWithPeer(ctx, hreq, treq, start, ttl, p, sender)
   607  	// Call the onFinish method before returning (with the error from call with peer)
   608  	onFinish(err)
   609  	return hres, err
   610  }
   611  
   612  func (o *Outbound) doWithPeer(
   613  	ctx context.Context,
   614  	hreq *http.Request,
   615  	treq *transport.Request,
   616  	start time.Time,
   617  	ttl time.Duration,
   618  	p *httpPeer,
   619  	sender sender,
   620  ) (*http.Response, error) {
   621  	hreq.URL.Host = p.HostPort()
   622  
   623  	response, err := sender.Do(hreq.WithContext(ctx))
   624  	if err != nil {
   625  		// Workaround borrowed from ctxhttp until
   626  		// https://github.com/golang/go/issues/17711 is resolved.
   627  		select {
   628  		case <-ctx.Done():
   629  			err = ctx.Err()
   630  		default:
   631  		}
   632  		if err == context.DeadlineExceeded {
   633  			// Note that the connection experienced a time out, which may
   634  			// indicate that the connection is half-open, that the destination
   635  			// died without sending a TCP FIN packet.
   636  			p.onSuspect()
   637  
   638  			end := time.Now()
   639  			return nil, yarpcerrors.Newf(
   640  				yarpcerrors.CodeDeadlineExceeded,
   641  				"client timeout for procedure %q of service %q after %v",
   642  				treq.Procedure, treq.Service, end.Sub(start))
   643  		}
   644  
   645  		// Note that the connection may have been lost so the peer connection
   646  		// maintenance loop resumes probing for availability.
   647  		p.onDisconnected()
   648  
   649  		return nil, yarpcerrors.Newf(yarpcerrors.CodeUnknown, "unknown error from http client: %s", err.Error())
   650  	}
   651  
   652  	return response, nil
   653  }
   654  
   655  // Introspect returns basic status about this outbound.
   656  func (o *Outbound) Introspect() introspection.OutboundStatus {
   657  	state := "Stopped"
   658  	if o.IsRunning() {
   659  		state = "Running"
   660  	}
   661  	var chooser introspection.ChooserStatus
   662  	if i, ok := o.chooser.(introspection.IntrospectableChooser); ok {
   663  		chooser = i.Introspect()
   664  	} else {
   665  		chooser = introspection.ChooserStatus{
   666  			Name: "Introspection not available",
   667  		}
   668  	}
   669  	return introspection.OutboundStatus{
   670  		Transport: "http",
   671  		Endpoint:  o.urlTemplate.String(),
   672  		State:     state,
   673  		Chooser:   chooser,
   674  	}
   675  }