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 }