Skip to content

Commit 1c69d08

Browse files
authored
fix: don't expect response to be json in endpointcreds provider (#2381)
1 parent 3bd97c0 commit 1c69d08

File tree

5 files changed

+173
-26
lines changed

5 files changed

+173
-26
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"id": "018d3cef-4def-4b01-9c5a-c7c60555b7e3",
3+
"type": "bugfix",
4+
"description": "Don't expect error responses to have a JSON payload in the endpointcreds provider.",
5+
"modules": [
6+
"credentials"
7+
]
8+
}

credentials/endpointcreds/internal/client/client.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,16 @@ func New(options Options, optFns ...func(*Options)) *Client {
6262
}
6363

6464
if options.Retryer == nil {
65-
options.Retryer = retry.NewStandard()
65+
// Amazon-owned implementations of this endpoint are known to sometimes
66+
// return plaintext responses (i.e. no Code) like normal, add a few
67+
// additional status codes
68+
options.Retryer = retry.NewStandard(func(o *retry.StandardOptions) {
69+
o.Retryables = append(o.Retryables, retry.RetryableHTTPStatusCode{
70+
Codes: map[int]struct{}{
71+
http.StatusTooManyRequests: {},
72+
},
73+
})
74+
})
6675
}
6776

6877
for _, fn := range optFns {
@@ -122,9 +131,10 @@ type GetCredentialsOutput struct {
122131

123132
// EndpointError is an error returned from the endpoint service
124133
type EndpointError struct {
125-
Code string `json:"code"`
126-
Message string `json:"message"`
127-
Fault smithy.ErrorFault `json:"-"`
134+
Code string `json:"code"`
135+
Message string `json:"message"`
136+
Fault smithy.ErrorFault `json:"-"`
137+
statusCode int `json:"-"`
128138
}
129139

130140
// Error is the error mesage string
@@ -146,3 +156,8 @@ func (e *EndpointError) ErrorMessage() string {
146156
func (e *EndpointError) ErrorFault() smithy.ErrorFault {
147157
return e.Fault
148158
}
159+
160+
// HTTPStatusCode implements retry.HTTPStatusCode.
161+
func (e *EndpointError) HTTPStatusCode() int {
162+
return e.statusCode
163+
}

credentials/endpointcreds/internal/client/client_test.go

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,23 @@ import (
1818

1919
func TestClient_GetCredentials(t *testing.T) {
2020
cases := map[string]struct {
21-
Token string
22-
RelativeURI string
23-
ResponseCode int
24-
ResponseBody []byte
25-
ExpectResult *GetCredentialsOutput
26-
ExpectErr bool
27-
ValidateRequest func(*testing.T, *http.Request)
28-
ValidateError func(*testing.T, error) bool
21+
Token string
22+
RelativeURI string
23+
ResponseCode int
24+
ResponseBody []byte
25+
ResponseContentType string
26+
ExpectResult *GetCredentialsOutput
27+
ExpectErr bool
28+
ValidateRequest func(*testing.T, *http.Request)
29+
ValidateError func(*testing.T, error) bool
2930
}{
3031
"success static": {
3132
ResponseCode: 200,
3233
ResponseBody: []byte(` {
3334
"AccessKeyId" : "FooKey",
3435
"SecretAccessKey" : "FooSecret"
3536
}`),
37+
ResponseContentType: "application/json",
3638
ExpectResult: &GetCredentialsOutput{
3739
AccessKeyID: "FooKey",
3840
SecretAccessKey: "FooSecret",
@@ -45,6 +47,7 @@ func TestClient_GetCredentials(t *testing.T) {
4547
"AccessKeyId" : "FooKey",
4648
"SecretAccessKey" : "FooSecret"
4749
}`),
50+
ResponseContentType: "application/json",
4851
ExpectResult: &GetCredentialsOutput{
4952
AccessKeyID: "FooKey",
5053
SecretAccessKey: "FooSecret",
@@ -59,6 +62,7 @@ func TestClient_GetCredentials(t *testing.T) {
5962
"Token": "FooToken",
6063
"Expiration": "2016-02-25T06:03:31Z"
6164
}`),
65+
ResponseContentType: "application/json",
6266
ExpectResult: &GetCredentialsOutput{
6367
AccessKeyID: "FooKey",
6468
SecretAccessKey: "FooSecret",
@@ -76,6 +80,7 @@ func TestClient_GetCredentials(t *testing.T) {
7680
"AccessKeyId" : "FooKey",
7781
"SecretAccessKey" : "FooSecret"
7882
}`),
83+
ResponseContentType: "application/json",
7984
ValidateRequest: func(t *testing.T, r *http.Request) {
8085
t.Helper()
8186
if e, a := "/path/to/thing", r.URL.Path; e != a {
@@ -96,7 +101,8 @@ func TestClient_GetCredentials(t *testing.T) {
96101
"code": "Unauthorized",
97102
"message": "not authorized for endpoint"
98103
}`),
99-
ExpectErr: true,
104+
ResponseContentType: "application/json",
105+
ExpectErr: true,
100106
ValidateError: func(t *testing.T, err error) (ok bool) {
101107
t.Helper()
102108
var apiError smithy.APIError
@@ -126,7 +132,8 @@ func TestClient_GetCredentials(t *testing.T) {
126132
"code": "InternalError",
127133
"message": "an error occurred"
128134
}`),
129-
ExpectErr: true,
135+
ResponseContentType: "application/json",
136+
ExpectErr: true,
130137
ValidateError: func(t *testing.T, err error) (ok bool) {
131138
t.Helper()
132139
var apiError smithy.APIError
@@ -151,13 +158,28 @@ func TestClient_GetCredentials(t *testing.T) {
151158
},
152159
},
153160
"non-json error response": {
154-
ResponseCode: 500,
155-
ResponseBody: []byte(`<html><body>unexpected message format</body></html>`),
156-
ExpectErr: true,
161+
ResponseCode: 500,
162+
ResponseBody: []byte(`<html><body>unexpected message format</body></html>`),
163+
ResponseContentType: "text/html",
164+
ExpectErr: true,
157165
ValidateError: func(t *testing.T, err error) (ok bool) {
158166
t.Helper()
159-
if e, a := "failed to decode error message", err.Error(); !strings.Contains(a, e) {
160-
t.Errorf("expect %v, got %v", e, a)
167+
var apiError smithy.APIError
168+
if errors.As(err, &apiError) {
169+
if e, a := "", apiError.ErrorCode(); e != a {
170+
t.Errorf("expect %v, got %v", e, a)
171+
ok = false
172+
}
173+
if e, a := "<html><body>unexpected message format</body></html>", apiError.ErrorMessage(); e != a {
174+
t.Errorf("expect %v, got %v", e, a)
175+
ok = false
176+
}
177+
if e, a := smithy.FaultServer, apiError.ErrorFault(); e != a {
178+
t.Errorf("expect %v, got %v", e, a)
179+
ok = false
180+
}
181+
} else {
182+
t.Errorf("expect %T error type, got %T: %v", apiError, err, err)
161183
ok = false
162184
}
163185
return ok
@@ -177,6 +199,7 @@ func TestClient_GetCredentials(t *testing.T) {
177199

178200
actualReq.Body = ioutil.NopCloser(bytes.NewReader(buf.Bytes()))
179201

202+
w.Header().Set("Content-Type", tt.ResponseContentType)
180203
w.WriteHeader(tt.ResponseCode)
181204
w.Write(tt.ResponseBody)
182205
}))

credentials/endpointcreds/internal/client/middleware.go

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"io"
78
"net/url"
89

910
"github.com/aws/smithy-go"
@@ -104,17 +105,44 @@ func (d *deserializeOpGetCredential) HandleDeserialize(ctx context.Context, in s
104105
}
105106

106107
func deserializeError(response *smithyhttp.Response) error {
107-
var errShape *EndpointError
108-
err := json.NewDecoder(response.Body).Decode(&errShape)
108+
// we could be talking to anything, json isn't guaranteed
109+
// see https://github.com/aws/aws-sdk-go-v2/issues/2316
110+
if response.Header.Get("Content-Type") == "application/json" {
111+
return deserializeJSONError(response)
112+
}
113+
114+
msg, err := io.ReadAll(response.Body)
109115
if err != nil {
110-
return &smithy.DeserializationError{Err: fmt.Errorf("failed to decode error message, %w", err)}
116+
return &smithy.DeserializationError{
117+
Err: fmt.Errorf("read response, %w", err),
118+
}
119+
}
120+
121+
return &EndpointError{
122+
// no sensible value for Code
123+
Message: string(msg),
124+
Fault: stof(response.StatusCode),
125+
statusCode: response.StatusCode,
111126
}
127+
}
112128

113-
if response.StatusCode >= 500 {
114-
errShape.Fault = smithy.FaultServer
115-
} else {
116-
errShape.Fault = smithy.FaultClient
129+
func deserializeJSONError(response *smithyhttp.Response) error {
130+
var errShape *EndpointError
131+
if err := json.NewDecoder(response.Body).Decode(&errShape); err != nil {
132+
return &smithy.DeserializationError{
133+
Err: fmt.Errorf("failed to decode error message, %w", err),
134+
}
117135
}
118136

137+
errShape.Fault = stof(response.StatusCode)
138+
errShape.statusCode = response.StatusCode
119139
return errShape
120140
}
141+
142+
// maps HTTP status code to smithy ErrorFault
143+
func stof(code int) smithy.ErrorFault {
144+
if code >= 500 {
145+
return smithy.FaultServer
146+
}
147+
return smithy.FaultClient
148+
}

credentials/endpointcreds/provider_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"errors"
77
"fmt"
8+
"io"
89
"io/ioutil"
910
"net/http"
1011
"strings"
@@ -201,6 +202,9 @@ func TestFailedRetrieveCredentials(t *testing.T) {
201202
"code": "Error",
202203
"message": "Message"
203204
}`))),
205+
Header: http.Header{
206+
"Content-Type": {"application/json"},
207+
},
204208
}, nil
205209
})
206210
})
@@ -238,3 +242,72 @@ func TestFailedRetrieveCredentials(t *testing.T) {
238242
t.Errorf("expect empty creds not to be expired")
239243
}
240244
}
245+
246+
type mockClientN struct {
247+
responses []*http.Response
248+
index int
249+
}
250+
251+
func (c *mockClientN) Do(r *http.Request) (*http.Response, error) {
252+
resp := c.responses[c.index]
253+
c.index++
254+
return resp, nil
255+
}
256+
257+
func TestRetryHTTPStatusCode(t *testing.T) {
258+
expTime := time.Now().UTC().Add(1 * time.Hour).Format("2006-01-02T15:04:05Z")
259+
credsResp := fmt.Sprintf(`{"AccessKeyID":"AKID","SecretAccessKey":"SECRET","Token":"TOKEN","Expiration":"%s"}`, expTime)
260+
261+
p := endpointcreds.New("http://127.0.0.1", func(o *endpointcreds.Options) {
262+
o.HTTPClient = &mockClientN{
263+
responses: []*http.Response{
264+
{
265+
StatusCode: 429,
266+
Body: io.NopCloser(strings.NewReader("You have made too many requests.")),
267+
Header: http.Header{
268+
"Content-Type": {"text/plain"},
269+
},
270+
},
271+
{
272+
StatusCode: 500,
273+
Body: io.NopCloser(strings.NewReader("Internal server error.")),
274+
Header: http.Header{
275+
"Content-Type": {"text/plain"},
276+
},
277+
},
278+
{
279+
StatusCode: 200,
280+
Body: ioutil.NopCloser(strings.NewReader(credsResp)),
281+
Header: http.Header{
282+
"Content-Type": {"application/json"},
283+
},
284+
},
285+
},
286+
}
287+
})
288+
289+
creds, err := p.Retrieve(context.Background())
290+
if err != nil {
291+
t.Fatalf("expect no error, got %v", err)
292+
}
293+
294+
if e, a := "AKID", creds.AccessKeyID; e != a {
295+
t.Errorf("expect %v, got %v", e, a)
296+
}
297+
if e, a := "SECRET", creds.SecretAccessKey; e != a {
298+
t.Errorf("expect %v, got %v", e, a)
299+
}
300+
if e, a := "TOKEN", creds.SessionToken; e != a {
301+
t.Errorf("expect %v, got %v", e, a)
302+
}
303+
if creds.Expired() {
304+
t.Errorf("expect not expired")
305+
}
306+
307+
sdk.NowTime = func() time.Time {
308+
return time.Now().Add(2 * time.Hour)
309+
}
310+
if !creds.Expired() {
311+
t.Errorf("expect to be expired")
312+
}
313+
}

0 commit comments

Comments
 (0)