Skip to content

Commit 19a7972

Browse files
authored
cmd error parsing: show a "try again after" message when a response sets a Retry-After header (#1198)
1 parent 05260ea commit 19a7972

File tree

2 files changed

+46
-7
lines changed

2 files changed

+46
-7
lines changed

cmd/cmd.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"net/http"
77
"regexp"
8+
"strconv"
89
"strings"
910

1011
"io"
@@ -80,6 +81,22 @@ func validateUserConfig(cfg *viper.Viper) error {
8081
// decodedAPIError decodes and returns the error message from the API response.
8182
// If the message is blank, it returns a fallback message with the status code.
8283
func decodedAPIError(resp *http.Response) error {
84+
// First and foremost, handle Retry-After headers; if set, show this to the user.
85+
if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" {
86+
// The Retry-After header can be an HTTP Date or delay seconds.
87+
// The date can be used as-is. The delay seconds should have "seconds" appended.
88+
if delay, err := strconv.Atoi(retryAfter); err == nil {
89+
retryAfter = fmt.Sprintf("%d seconds", delay)
90+
}
91+
return fmt.Errorf(
92+
"request failed with status %s; please try again after %s",
93+
resp.Status,
94+
retryAfter,
95+
)
96+
}
97+
98+
// Check for JSON data. On non-JSON data, show the status and content type then bail.
99+
// Otherwise, extract the message details from the JSON.
83100
if contentType := resp.Header.Get("Content-Type"); !jsonContentTypeRe.MatchString(contentType) {
84101
return fmt.Errorf(
85102
"expected response with Content-Type \"application/json\" but got status %q with Content-Type %q",

cmd/cmd_test.go

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ func (co capturedOutput) reset() {
123123
Err = co.oldErr
124124
}
125125

126-
func errorResponse(contentType string, body string) *http.Response {
126+
func errorResponse418(contentType string, body string) *http.Response {
127127
response := &http.Response{
128128
Status: "418 I'm a teapot",
129129
StatusCode: 418,
@@ -135,35 +135,57 @@ func errorResponse(contentType string, body string) *http.Response {
135135
return response
136136
}
137137

138+
func errorResponse429(retryAfter string) *http.Response {
139+
body := ""
140+
response := &http.Response{
141+
Status: "429 Too Many Requests",
142+
StatusCode: 429,
143+
Header: make(http.Header),
144+
Body: ioutil.NopCloser(strings.NewReader(body)),
145+
ContentLength: int64(len(body)),
146+
}
147+
response.Header.Set("Content-Type", "text/plain")
148+
response.Header.Set("Retry-After", retryAfter)
149+
return response
150+
}
151+
138152
func TestDecodeErrorResponse(t *testing.T) {
139153
testCases := []struct {
140154
response *http.Response
141155
wantMessage string
142156
}{
143157
{
144-
response: errorResponse("text/html", "Time for tea"),
158+
response: errorResponse418("text/html", "Time for tea"),
145159
wantMessage: `expected response with Content-Type "application/json" but got status "418 I'm a teapot" with Content-Type "text/html"`,
146160
},
147161
{
148-
response: errorResponse("application/json", `{"error": {"type": "json", "valid": no}}`),
162+
response: errorResponse418("application/json", `{"error": {"type": "json", "valid": no}}`),
149163
wantMessage: "failed to parse API error response: invalid character 'o' in literal null (expecting 'u')",
150164
},
151165
{
152-
response: errorResponse("application/json", `{"error": {"type": "track_ambiguous", "message": "message", "possible_track_ids": ["a", "b"]}}`),
166+
response: errorResponse418("application/json", `{"error": {"type": "track_ambiguous", "message": "message", "possible_track_ids": ["a", "b"]}}`),
153167
wantMessage: "message: a, b",
154168
},
155169
{
156-
response: errorResponse("application/json", `{"error": {"message": "message"}}`),
170+
response: errorResponse418("application/json", `{"error": {"message": "message"}}`),
157171
wantMessage: "message",
158172
},
159173
{
160-
response: errorResponse("application/problem+json", `{"error": {"message": "new json format"}}`),
174+
response: errorResponse418("application/problem+json", `{"error": {"message": "new json format"}}`),
161175
wantMessage: "new json format",
162176
},
163177
{
164-
response: errorResponse("application/json", `{"error": {}}`),
178+
response: errorResponse418("application/json", `{"error": {}}`),
165179
wantMessage: "unexpected API response: 418",
166180
},
181+
{
182+
response: errorResponse429("30"),
183+
wantMessage: "request failed with status 429 Too Many Requests; please try again after 30 seconds",
184+
},
185+
{
186+
response: errorResponse429("Wed, 21 Oct 2015 07:28:00 GMT"),
187+
wantMessage: "request failed with status 429 Too Many Requests; please try again after Wed, 21 Oct 2015 07:28:00 GMT",
188+
},
167189
}
168190
tc := testCases[0]
169191
got := decodedAPIError(tc.response)

0 commit comments

Comments
 (0)