Skip to content

Commit a357131

Browse files
authored
aws: Add default HTTP client instead of http.DefaultClient/Transport (#315)
Adds a new BuildableHTTPClient type to the SDK's aws package. The type uses the builder pattern with immutable changes. Modifications to the buildable client create copies of the client. Adds a HTTPClient interface to the aws package that the SDK will use as an abstraction over the specific HTTP client implementation. The SDK will default to the BuildableHTTPClient, but a *http.Client can be also provided for custom configuration. When the SDK's aws.Config.HTTPClient value is a BuildableHTTPClient the SDK will be able to use API client specific request timeout options. Fix #279 Fix #269
1 parent 4f5e4b5 commit a357131

18 files changed

+594
-228
lines changed

aws/client.go

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
type Metadata struct {
99
ServiceName string
1010
ServiceID string
11-
EndpointsID string
11+
EndpointsID string
1212
APIVersion string
1313

1414
Endpoint string
@@ -36,15 +36,15 @@ type Client struct {
3636
LogLevel LogLevel
3737
Logger Logger
3838

39-
HTTPClient *http.Client
39+
HTTPClient HTTPClient
4040
}
4141

4242
// NewClient will return a pointer to a new initialized service client.
4343
func NewClient(cfg Config, metadata Metadata) *Client {
4444
svc := &Client{
4545
Metadata: metadata,
4646

47-
// TODO remove config when request reqfactored
47+
// TODO remove config when request refactored
4848
Config: cfg,
4949

5050
Region: cfg.Region,
@@ -57,6 +57,10 @@ func NewClient(cfg Config, metadata Metadata) *Client {
5757
Logger: cfg.Logger,
5858
}
5959

60+
if c, ok := svc.Config.HTTPClient.(*http.Client); ok {
61+
svc.Config.HTTPClient = wrapWithoutRedirect(c)
62+
}
63+
6064
retryer := cfg.Retryer
6165
if retryer == nil {
6266
// TODO need better way of specifing default num retries
@@ -85,3 +89,60 @@ func (c *Client) AddDebugHandlers() {
8589
c.Handlers.Send.PushFrontNamed(NamedHandler{Name: "awssdk.client.LogRequest", Fn: logRequest})
8690
c.Handlers.Send.PushBackNamed(NamedHandler{Name: "awssdk.client.LogResponse", Fn: logResponse})
8791
}
92+
93+
func wrapWithoutRedirect(c *http.Client) *http.Client {
94+
tr := c.Transport
95+
if tr == nil {
96+
tr = http.DefaultTransport
97+
}
98+
99+
cc := *c
100+
cc.CheckRedirect = limitedRedirect
101+
cc.Transport = stubBadHTTPRedirectTransport{
102+
tr: tr,
103+
}
104+
105+
return &cc
106+
}
107+
108+
func limitedRedirect(r *http.Request, via []*http.Request) error {
109+
// Request.Response, in CheckRedirect is the response that is triggering
110+
// the redirect.
111+
resp := r.Response
112+
if r.URL.String() == stubBadHTTPRedirectLocation {
113+
resp.Header.Del(stubBadHTTPRedirectLocation)
114+
return http.ErrUseLastResponse
115+
}
116+
117+
switch resp.StatusCode {
118+
case 307, 308:
119+
// Only allow 307 and 308 redirects as they preserve the method.
120+
return nil
121+
}
122+
123+
return http.ErrUseLastResponse
124+
}
125+
126+
type stubBadHTTPRedirectTransport struct {
127+
tr http.RoundTripper
128+
}
129+
130+
const stubBadHTTPRedirectLocation = `https://amazonaws.com/badhttpredirectlocation`
131+
132+
func (t stubBadHTTPRedirectTransport) RoundTrip(r *http.Request) (*http.Response, error) {
133+
resp, err := t.tr.RoundTrip(r)
134+
if err != nil {
135+
return resp, err
136+
}
137+
138+
// TODO S3 is the only known service to return 301 without location header.
139+
// consider moving this to a S3 customization.
140+
switch resp.StatusCode {
141+
case 301, 302:
142+
if v := resp.Header.Get("Location"); len(v) == 0 {
143+
resp.Header.Set("Location", stubBadHTTPRedirectLocation)
144+
}
145+
}
146+
147+
return resp, err
148+
}

aws/config.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
package aws
22

3-
import (
4-
"net/http"
5-
)
6-
73
// A Config provides service configuration for service clients.
84
type Config struct {
95
// The region to send requests to. This parameter is required and must
@@ -24,9 +20,13 @@ type Config struct {
2420
// to use based on region.
2521
EndpointResolver EndpointResolver
2622

27-
// The HTTP client to use when sending requests. Defaults to
28-
// `http.DefaultClient`.
29-
HTTPClient *http.Client
23+
// The HTTP Client the SDK's API clients will use to invoke HTTP requests.
24+
// The SDK defaults to a BuildableHTTPClient allowing API clients to create
25+
// copies of the HTTP Client for service specific customizations.
26+
//
27+
// Use a (*http.Client) for custom behavior. Using a custom http.Client
28+
// will prevent the SDK from modifying the HTTP client.
29+
HTTPClient HTTPClient
3030

3131
// TODO document
3232
Handlers Handlers

aws/defaults/defaults.go

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@ package defaults
99

1010
import (
1111
"log"
12-
"net/http"
1312
"os"
14-
"time"
1513

1614
"github.com/aws/aws-sdk-go-v2/aws"
1715
"github.com/aws/aws-sdk-go-v2/aws/endpoints"
@@ -55,17 +53,8 @@ func Config() aws.Config {
5553
// HTTPClient will return a new HTTP Client configured for the SDK.
5654
//
5755
// Does not use http.DefaultClient nor http.DefaultTransport.
58-
func HTTPClient() *http.Client {
59-
return &http.Client{
60-
Transport: &http.Transport{
61-
Proxy: http.ProxyFromEnvironment,
62-
MaxIdleConns: 100,
63-
MaxIdleConnsPerHost: 10,
64-
IdleConnTimeout: 30 * time.Second,
65-
TLSHandshakeTimeout: 10 * time.Second,
66-
ExpectContinueTimeout: 5 * time.Second,
67-
},
68-
}
56+
func HTTPClient() aws.HTTPClient {
57+
return aws.NewBuildableHTTPClient()
6958
}
7059

7160
// Handlers returns the default request handlers.

aws/defaults/handlers.go

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,9 @@ var ValidateReqSigHandler = aws.NamedHandler{
9191
var SendHandler = aws.NamedHandler{
9292
Name: "core.SendHandler",
9393
Fn: func(r *aws.Request) {
94-
sender := sendFollowRedirects
95-
if r.DisableFollowRedirects {
96-
sender = sendWithoutFollowRedirects
97-
}
9894

95+
// TODO remove this complexity the SDK's built http.Request should
96+
// set Request.Body to nil, if there is no body to send. #318
9997
if aws.NoBody == r.HTTPRequest.Body {
10098
// Strip off the request body if the NoBody reader was used as a
10199
// place holder for a request body. This prevents the SDK from
@@ -113,26 +111,13 @@ var SendHandler = aws.NamedHandler{
113111
}
114112

115113
var err error
116-
r.HTTPResponse, err = sender(r)
114+
r.HTTPResponse, err = r.Config.HTTPClient.Do(r.HTTPRequest)
117115
if err != nil {
118116
handleSendError(r, err)
119117
}
120118
},
121119
}
122120

123-
func sendFollowRedirects(r *aws.Request) (*http.Response, error) {
124-
return r.Config.HTTPClient.Do(r.HTTPRequest)
125-
}
126-
127-
func sendWithoutFollowRedirects(r *aws.Request) (*http.Response, error) {
128-
transport := r.Config.HTTPClient.Transport
129-
if transport == nil {
130-
transport = http.DefaultTransport
131-
}
132-
133-
return transport.RoundTrip(r.HTTPRequest)
134-
}
135-
136121
func handleSendError(r *aws.Request, err error) {
137122
// Prevent leaking if an HTTPResponse was returned. Clean up
138123
// the body.

aws/defaults/handlers_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,6 @@ func TestSendWithoutFollowRedirects(t *testing.T) {
252252
Name: "Operation",
253253
HTTPPath: "/original",
254254
}, nil, nil)
255-
r.DisableFollowRedirects = true
256255

257256
err := r.Send()
258257
if err != nil {

aws/ec2metadata/api_client.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ import (
1111
"bytes"
1212
"errors"
1313
"io"
14+
"net"
1415
"net/http"
1516
"os"
1617
"strings"
18+
"time"
1719

1820
"github.com/aws/aws-sdk-go-v2/aws"
1921
"github.com/aws/aws-sdk-go-v2/aws/awserr"
@@ -34,13 +36,26 @@ type Client struct {
3436
// // Create a Client client from just a config.
3537
// svc := ec2metadata.New(cfg)
3638
func New(config aws.Config) *Client {
39+
if c, ok := config.HTTPClient.(*aws.BuildableHTTPClient); ok {
40+
// TODO consider moving this to a client configuration via client builder
41+
// instead automatically being set.
42+
43+
// Use a custom Dial timeout for the EC2 Metadata service to account
44+
// for the possibility the application might not be running in an
45+
// environment with the service present. The client should fail fast in
46+
// this case.
47+
config.HTTPClient = c.WithDialerOptions(func(d *net.Dialer) {
48+
d.Timeout = 5 * time.Second
49+
})
50+
}
51+
3752
svc := &Client{
3853
Client: aws.NewClient(
3954
config,
4055
aws.Metadata{
4156
ServiceName: "EC2 Instance Metadata",
4257
ServiceID: "EC2InstanceMetadata",
43-
EndpointsID: "ec2metadata",
58+
EndpointsID: "ec2metadata",
4459
APIVersion: "latest",
4560
},
4661
),
@@ -72,10 +87,6 @@ func New(config aws.Config) *Client {
7287
return svc
7388
}
7489

75-
func httpClientZero(c *http.Client) bool {
76-
return c == nil || (c.Transport == nil && c.CheckRedirect == nil && c.Jar == nil && c.Timeout == 0)
77-
}
78-
7990
type metadataOutput struct {
8091
Content string
8192
}

aws/external/http_client.go

Lines changed: 0 additions & 42 deletions
This file was deleted.

aws/external/http_client_test.go

Lines changed: 0 additions & 46 deletions
This file was deleted.

0 commit comments

Comments
 (0)