Skip to content

Commit 69fa2a5

Browse files
authored
Request response error handling now unwraps both JsonApi & json errors (#1304)
* handle both jsonapi and json error unwrapping
1 parent 0fafe76 commit 69fa2a5

File tree

3 files changed

+101
-10
lines changed

3 files changed

+101
-10
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Unreleased
22

3+
## Bug Fixes
4+
* Improve API error handling to decode both JSON:API error objects and regular JSON errors arrays by @uk1288 [#1304](https://github.com/hashicorp/go-tfe/pull/1304)
5+
36
## Enhancements
47
* Adds the `ProviderType` field to `AdminSAMLSetting` and `AdminSAMLSettingsUpdateOptions` to support the `provider-type` SAML setting.
58

tfe.go

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,22 +1099,33 @@ func checkResponseCode(r *http.Response) error {
10991099
func decodeErrorPayload(r *http.Response) ([]string, error) {
11001100
// Decode the error payload.
11011101
var errs []string
1102-
errPayload := &jsonapi.ErrorsPayload{}
1103-
err := json.NewDecoder(r.Body).Decode(errPayload)
1104-
if err != nil || len(errPayload.Errors) == 0 {
1102+
body, err := io.ReadAll(r.Body)
1103+
if err != nil {
11051104
return errs, errors.New(r.Status)
11061105
}
11071106

1108-
// Parse and format the errors.
1109-
for _, e := range errPayload.Errors {
1110-
if e.Detail == "" {
1111-
errs = append(errs, e.Title)
1112-
} else {
1113-
errs = append(errs, fmt.Sprintf("%s\n\n%s", e.Title, e.Detail))
1107+
// attempt JSON:API error payloads unwrapping
1108+
errPayload := &jsonapi.ErrorsPayload{}
1109+
if err := json.Unmarshal(body, errPayload); err == nil && len(errPayload.Errors) > 0 {
1110+
for _, e := range errPayload.Errors {
1111+
if e.Detail == "" {
1112+
errs = append(errs, e.Title)
1113+
} else {
1114+
errs = append(errs, fmt.Sprintf("%s\n\n%s", e.Title, e.Detail))
1115+
}
11141116
}
1117+
return errs, nil
1118+
}
1119+
1120+
// attempt JSON error payloads unwrapping: like {"errors":["..."]}.
1121+
var rawErrs struct {
1122+
Errors []string `json:"errors"`
1123+
}
1124+
if err := json.Unmarshal(body, &rawErrs); err == nil && len(rawErrs.Errors) > 0 {
1125+
return rawErrs.Errors, nil
11151126
}
11161127

1117-
return errs, nil
1128+
return errs, errors.New(r.Status)
11181129
}
11191130

11201131
func errorPayloadContains(payloadErrors []string, match string) bool {

tfe_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"context"
99
"encoding/json"
1010
"fmt"
11+
"io"
1112
"net/http"
1213
"net/http/httptest"
1314
"os"
@@ -124,6 +125,82 @@ func Test_unmarshalResponse(t *testing.T) {
124125
})
125126
}
126127

128+
func Test_decodeErrorPayload(t *testing.T) {
129+
t.Parallel()
130+
131+
t.Run("with jsonapi errors payload", func(t *testing.T) {
132+
resp := &http.Response{
133+
Status: "400 Bad Request",
134+
StatusCode: http.StatusBadRequest,
135+
Body: io.NopCloser(bytes.NewBufferString(
136+
`{"errors":[{"title":"org name invalid","detail":"Org name is invalid"}]}`,
137+
)),
138+
}
139+
140+
errs, err := decodeErrorPayload(resp)
141+
require.NoError(t, err)
142+
require.Len(t, errs, 1)
143+
assert.Equal(t, "org name invalid\n\nOrg name is invalid", errs[0])
144+
})
145+
146+
t.Run("with regular json errors payload", func(t *testing.T) {
147+
resp := &http.Response{
148+
Status: "400 Bad Request",
149+
StatusCode: http.StatusBadRequest,
150+
Body: io.NopCloser(bytes.NewBufferString(
151+
`{"errors":["Unsupported GPG Key algorithm. Supported key algorithms are [RSA, DSA]"]}`,
152+
)),
153+
}
154+
155+
errs, err := decodeErrorPayload(resp)
156+
require.NoError(t, err)
157+
require.Len(t, errs, 1)
158+
assert.Equal(t, "Unsupported GPG Key algorithm. Supported key algorithms are [RSA, DSA]", errs[0])
159+
})
160+
161+
t.Run("with non-json error body", func(t *testing.T) {
162+
resp := &http.Response{
163+
Status: "400 Bad Request",
164+
StatusCode: http.StatusBadRequest,
165+
Body: io.NopCloser(bytes.NewBufferString("this is not json")),
166+
}
167+
168+
errs, err := decodeErrorPayload(resp)
169+
require.EqualError(t, err, "400 Bad Request")
170+
assert.Empty(t, errs)
171+
})
172+
}
173+
174+
func Test_checkResponseCode(t *testing.T) {
175+
t.Parallel()
176+
177+
t.Run("returns regular json error message", func(t *testing.T) {
178+
resp := &http.Response{
179+
Status: "400 Bad Request",
180+
StatusCode: http.StatusBadRequest,
181+
Body: io.NopCloser(bytes.NewBufferString(
182+
`{"errors":["Unsupported GPG Key algorithm. Supported key algorithms are [RSA, DSA]"]}`,
183+
)),
184+
}
185+
186+
err := checkResponseCode(resp)
187+
require.EqualError(t, err, "Unsupported GPG Key algorithm. Supported key algorithms are [RSA, DSA]")
188+
})
189+
190+
t.Run("still maps invalid include message", func(t *testing.T) {
191+
resp := &http.Response{
192+
Status: "400 Bad Request",
193+
StatusCode: http.StatusBadRequest,
194+
Body: io.NopCloser(bytes.NewBufferString(
195+
`{"errors":["include parameter is invalid"]}`,
196+
)),
197+
}
198+
199+
err := checkResponseCode(resp)
200+
assert.ErrorIs(t, err, ErrInvalidIncludeValue)
201+
})
202+
}
203+
127204
func Test_BaseURL(t *testing.T) {
128205
t.Parallel()
129206
client, err := NewClient(&Config{

0 commit comments

Comments
 (0)