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