client.go raw

   1  package http
   2  
   3  import (
   4  	"context"
   5  	"fmt"
   6  	"net/http"
   7  
   8  	smithy "github.com/aws/smithy-go"
   9  	"github.com/aws/smithy-go/metrics"
  10  	"github.com/aws/smithy-go/middleware"
  11  	"github.com/aws/smithy-go/tracing"
  12  )
  13  
  14  // ClientDo provides the interface for custom HTTP client implementations.
  15  type ClientDo interface {
  16  	Do(*http.Request) (*http.Response, error)
  17  }
  18  
  19  // ClientDoFunc provides a helper to wrap a function as an HTTP client for
  20  // round tripping requests.
  21  type ClientDoFunc func(*http.Request) (*http.Response, error)
  22  
  23  // Do will invoke the underlying func, returning the result.
  24  func (fn ClientDoFunc) Do(r *http.Request) (*http.Response, error) {
  25  	return fn(r)
  26  }
  27  
  28  // ClientHandler wraps a client that implements the HTTP Do method. Standard
  29  // implementation is http.Client.
  30  type ClientHandler struct {
  31  	client ClientDo
  32  
  33  	Meter metrics.Meter // For HTTP client metrics.
  34  }
  35  
  36  // NewClientHandler returns an initialized middleware handler for the client.
  37  //
  38  // Deprecated: Use [NewClientHandlerWithOptions].
  39  func NewClientHandler(client ClientDo) ClientHandler {
  40  	return NewClientHandlerWithOptions(client)
  41  }
  42  
  43  // NewClientHandlerWithOptions returns an initialized middleware handler for the client
  44  // with applied options.
  45  func NewClientHandlerWithOptions(client ClientDo, opts ...func(*ClientHandler)) ClientHandler {
  46  	h := ClientHandler{
  47  		client: client,
  48  	}
  49  	for _, opt := range opts {
  50  		opt(&h)
  51  	}
  52  	if h.Meter == nil {
  53  		h.Meter = metrics.NopMeterProvider{}.Meter("")
  54  	}
  55  	return h
  56  }
  57  
  58  // Handle implements the middleware Handler interface, that will invoke the
  59  // underlying HTTP client. Requires the input to be a Smithy *Request. Returns
  60  // a smithy *Response, or error if the request failed.
  61  func (c ClientHandler) Handle(ctx context.Context, input interface{}) (
  62  	out interface{}, metadata middleware.Metadata, err error,
  63  ) {
  64  	ctx, span := tracing.StartSpan(ctx, "DoHTTPRequest")
  65  	defer span.End()
  66  
  67  	ctx, client, err := withMetrics(ctx, c.client, c.Meter)
  68  	if err != nil {
  69  		return nil, metadata, fmt.Errorf("instrument with HTTP metrics: %w", err)
  70  	}
  71  
  72  	req, ok := input.(*Request)
  73  	if !ok {
  74  		return nil, metadata, fmt.Errorf("expect Smithy http.Request value as input, got unsupported type %T", input)
  75  	}
  76  
  77  	builtRequest := req.Build(ctx)
  78  	if err := ValidateEndpointHost(builtRequest.Host); err != nil {
  79  		return nil, metadata, err
  80  	}
  81  
  82  	span.SetProperty("http.method", req.Method)
  83  	span.SetProperty("http.request_content_length", -1) // at least indicate unknown
  84  	length, ok, err := req.StreamLength()
  85  	if err != nil {
  86  		return nil, metadata, err
  87  	}
  88  	if ok {
  89  		span.SetProperty("http.request_content_length", length)
  90  	}
  91  
  92  	resp, err := client.Do(builtRequest)
  93  	if resp == nil {
  94  		// Ensure a http response value is always present to prevent unexpected
  95  		// panics.
  96  		resp = &http.Response{
  97  			Header: http.Header{},
  98  			Body:   http.NoBody,
  99  		}
 100  	}
 101  	if err != nil {
 102  		err = &RequestSendError{Err: err}
 103  
 104  		// Override the error with a context canceled error, if that was canceled.
 105  		select {
 106  		case <-ctx.Done():
 107  			err = &smithy.CanceledError{Err: ctx.Err()}
 108  		default:
 109  		}
 110  	}
 111  
 112  	// HTTP RoundTripper *should* close the request body. But this may not happen in a timely manner.
 113  	// So instead Smithy *Request Build wraps the body to be sent in a safe closer that will clear the
 114  	// stream reference so that it can be safely reused.
 115  	if builtRequest.Body != nil {
 116  		_ = builtRequest.Body.Close()
 117  	}
 118  
 119  	span.SetProperty("net.protocol.version", fmt.Sprintf("%d.%d", resp.ProtoMajor, resp.ProtoMinor))
 120  	span.SetProperty("http.status_code", resp.StatusCode)
 121  	span.SetProperty("http.response_content_length", resp.ContentLength)
 122  
 123  	return &Response{Response: resp}, metadata, err
 124  }
 125  
 126  // RequestSendError provides a generic request transport error. This error
 127  // should wrap errors making HTTP client requests.
 128  //
 129  // The ClientHandler will wrap the HTTP client's error if the client request
 130  // fails, and did not fail because of context canceled.
 131  type RequestSendError struct {
 132  	Err error
 133  }
 134  
 135  // ConnectionError returns that the error is related to not being able to send
 136  // the request, or receive a response from the service.
 137  func (e *RequestSendError) ConnectionError() bool {
 138  	return true
 139  }
 140  
 141  // Unwrap returns the underlying error, if there was one.
 142  func (e *RequestSendError) Unwrap() error {
 143  	return e.Err
 144  }
 145  
 146  func (e *RequestSendError) Error() string {
 147  	return fmt.Sprintf("request send failed, %v", e.Err)
 148  }
 149  
 150  // NopClient provides a client that ignores the request, and returns an empty
 151  // successful HTTP response value.
 152  type NopClient struct{}
 153  
 154  // Do ignores the request and returns a 200 status empty response.
 155  func (NopClient) Do(r *http.Request) (*http.Response, error) {
 156  	return &http.Response{
 157  		StatusCode: 200,
 158  		Header:     http.Header{},
 159  		Body:       http.NoBody,
 160  	}, nil
 161  }
 162