Skip to content

Commit 9e38100

Browse files
authored
⚙️ Map response errors to common errors (#121)
<!-- Copyright (C) 2020-2022 Arm Limited or its affiliates and Contributors. All rights reserved. SPDX-License-Identifier: Proprietary --> ### Description This is to have a better understanding of client errors ### Test Coverage <!-- Please put an `x` in the correct box e.g. `[x]` to indicate the testing coverage of this change. --> - [x] This change is covered by existing or additional automated tests. - [ ] Manual testing has been performed (and evidence provided) as automated testing was not feasible. - [ ] Additional tests are not required for this change (e.g. documentation update). --------- Co-authored-by: acabarbaye <[email protected]>
1 parent 3a38461 commit 9e38100

File tree

10 files changed

+209
-53
lines changed

10 files changed

+209
-53
lines changed

changes/20250820184330.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:gear: Map response errors to common errors

changes/20250820191610.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:camel: Upgrade dependencies

utils/api/api.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,16 @@ func CheckAPICallSuccess(ctx context.Context, errorContext string, resp *http.Re
4444
if !IsCallSuccessful(resp) {
4545
statusCode := 0
4646
errorMessage := strings.Builder{}
47+
respErr := commonerrors.ErrUnexpected
4748
if resp != nil {
4849
statusCode = resp.StatusCode
50+
respErr = errors.MapErrorToHTTPResponseCode(statusCode)
51+
if respErr == nil {
52+
respErr = commonerrors.ErrUnexpected
53+
}
4954
errorDetails, subErr := errors.FetchAPIErrorDescriptionWithContext(ctx, resp)
5055
if commonerrors.Ignore(subErr, commonerrors.ErrMarshalling) != nil {
51-
err = subErr
56+
err = commonerrors.Join(commonerrors.New(respErr, errorContext), subErr)
5257
return
5358
}
5459
if !reflection.IsEmpty(errorDetails) {
@@ -60,7 +65,7 @@ func CheckAPICallSuccess(ctx context.Context, errorContext string, resp *http.Re
6065
if apiErr != nil {
6166
extra = fmt.Sprintf("; %v", apiErr.Error())
6267
}
63-
err = fmt.Errorf("%v (%d): %v%v", errorContext, statusCode, errorMessage.String(), extra)
68+
err = commonerrors.Newf(respErr, "%v (%d): %v%v", errorContext, statusCode, errorMessage.String(), extra)
6469
}
6570
return
6671
}

utils/api/api_test.go

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,19 +59,41 @@ func TestCheckAPICallSuccess(t *testing.T) {
5959
t.Run("api call not successful", func(t *testing.T) {
6060
errMessage := "client error"
6161
parentCtx := context.Background()
62-
resp := _http.Response{StatusCode: 400, Body: io.NopCloser(bytes.NewReader([]byte("{\"message\": \"client error\",\"requestId\": \"761761721\"}")))}
62+
resp := _http.Response{StatusCode: _http.StatusBadRequest, Body: io.NopCloser(bytes.NewReader([]byte("{\"message\": \"client error\",\"requestId\": \"761761721\"}")))}
6363
actualErr := CheckAPICallSuccess(parentCtx, errMessage, &resp, errors.New(errMessage))
6464
expectedErr := "client error (400): API call error [request-id: 761761721] client error; client error"
65-
assert.Equal(t, actualErr.Error(), expectedErr)
65+
assert.Contains(t, actualErr.Error(), expectedErr)
66+
errortest.AssertError(t, actualErr, commonerrors.ErrInvalid)
67+
})
68+
69+
t.Run("api call not successful", func(t *testing.T) {
70+
errMessage := "client error"
71+
parentCtx := context.Background()
72+
resp := _http.Response{StatusCode: _http.StatusServiceUnavailable, Body: io.NopCloser(bytes.NewReader([]byte("{\"message\": \"client error\",\"requestId\": \"761761721\"}")))}
73+
actualErr := CheckAPICallSuccess(parentCtx, errMessage, &resp, errors.New(errMessage))
74+
expectedErr := "client error (503): API call error [request-id: 761761721] client error; client error"
75+
assert.Contains(t, actualErr.Error(), expectedErr)
76+
errortest.AssertError(t, actualErr, commonerrors.ErrUnavailable)
77+
})
78+
79+
t.Run("api call not successful", func(t *testing.T) {
80+
errMessage := "client error"
81+
parentCtx := context.Background()
82+
resp := _http.Response{StatusCode: _http.StatusUnauthorized, Body: io.NopCloser(bytes.NewReader([]byte("{\"message\": \"client error\",\"requestId\": \"761761721\"}")))}
83+
actualErr := CheckAPICallSuccess(parentCtx, errMessage, &resp, errors.New(errMessage))
84+
expectedErr := "client error (401): API call error [request-id: 761761721] client error; client error"
85+
assert.Contains(t, actualErr.Error(), expectedErr)
86+
errortest.AssertError(t, actualErr, commonerrors.ErrUnauthorised)
6687
})
6788

6889
t.Run("api call not successful (no JSON response)", func(t *testing.T) {
6990
errMessage := "response error"
7091
parentCtx := context.Background()
71-
resp := _http.Response{StatusCode: 403, Body: io.NopCloser(bytes.NewReader([]byte("<html><head><title>403 Forbidden</title></head></html>")))}
92+
resp := _http.Response{StatusCode: _http.StatusForbidden, Body: io.NopCloser(bytes.NewReader([]byte("<html><head><title>403 Forbidden</title></head></html>")))}
7293
actualErr := CheckAPICallSuccess(parentCtx, errMessage, &resp, errors.New("403 Forbidden"))
7394
expectedErr := "response error (403): <html><head><title>403 Forbidden</title></head></html>; 403 Forbidden"
74-
assert.Equal(t, actualErr.Error(), expectedErr)
95+
assert.Contains(t, actualErr.Error(), expectedErr)
96+
errortest.AssertError(t, actualErr, commonerrors.ErrForbidden)
7597
})
7698

7799
t.Run("no context error, api call successful", func(t *testing.T) {
@@ -105,7 +127,8 @@ func TestCallAndCheckSuccess(t *testing.T) {
105127
return nil, &resp, errors.New(errMessage)
106128
})
107129
expectedErr := "client error (400): API call error [request-id: 761761721] client error; client error"
108-
assert.Equal(t, actualErr.Error(), expectedErr)
130+
assert.Contains(t, actualErr.Error(), expectedErr)
131+
errortest.AssertError(t, actualErr, commonerrors.ErrInvalid)
109132
})
110133

111134
t.Run("api call successful, marshalling failed due to missing required field in response", func(t *testing.T) {
@@ -213,7 +236,8 @@ func TestGenericCallAndCheckSuccess(t *testing.T) {
213236
return nil, &resp, errors.New(errMessage)
214237
})
215238
expectedErr := "client error (400): API call error [request-id: 761761721] client error; client error"
216-
assert.Equal(t, actualErr.Error(), expectedErr)
239+
assert.Contains(t, actualErr.Error(), expectedErr)
240+
errortest.AssertError(t, actualErr, commonerrors.ErrInvalid)
217241
})
218242

219243
t.Run("api call successful but error marshalling", func(t *testing.T) {

utils/errors/mapping.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Copyright (C) 2020-2025 Arm Limited or its affiliates and Contributors. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package errors
6+
7+
import (
8+
"net/http"
9+
10+
"github.com/ARM-software/golang-utils/utils/commonerrors"
11+
)
12+
13+
// MapErrorToHTTPResponseCode maps a response status code to a common error.
14+
func MapErrorToHTTPResponseCode(statusCode int) error {
15+
if statusCode < http.StatusBadRequest {
16+
return nil
17+
}
18+
switch statusCode {
19+
case http.StatusBadRequest:
20+
return commonerrors.ErrInvalid
21+
case http.StatusUnauthorized:
22+
return commonerrors.ErrUnauthorised
23+
case http.StatusPaymentRequired:
24+
return commonerrors.ErrUnknown
25+
case http.StatusForbidden:
26+
return commonerrors.ErrForbidden
27+
case http.StatusNotFound:
28+
return commonerrors.ErrNotFound
29+
case http.StatusMethodNotAllowed:
30+
return commonerrors.ErrNotFound
31+
case http.StatusNotAcceptable:
32+
return commonerrors.ErrUnsupported
33+
case http.StatusProxyAuthRequired:
34+
return commonerrors.ErrUnauthorised
35+
case http.StatusRequestTimeout:
36+
return commonerrors.ErrTimeout
37+
case http.StatusConflict:
38+
return commonerrors.ErrConflict
39+
case http.StatusGone:
40+
return commonerrors.ErrNotFound
41+
case http.StatusLengthRequired:
42+
return commonerrors.ErrInvalid
43+
case http.StatusPreconditionFailed:
44+
return commonerrors.ErrCondition
45+
case http.StatusRequestEntityTooLarge:
46+
return commonerrors.ErrTooLarge
47+
case http.StatusRequestURITooLong:
48+
return commonerrors.ErrTooLarge
49+
case http.StatusUnsupportedMediaType:
50+
return commonerrors.ErrUnsupported
51+
case http.StatusRequestedRangeNotSatisfiable:
52+
return commonerrors.ErrOutOfRange
53+
case http.StatusExpectationFailed:
54+
return commonerrors.ErrUnsupported
55+
case http.StatusTeapot:
56+
return commonerrors.ErrUnknown
57+
case http.StatusMisdirectedRequest:
58+
return commonerrors.ErrUnsupported
59+
case http.StatusUnprocessableEntity:
60+
return commonerrors.ErrMarshalling
61+
case http.StatusLocked:
62+
return commonerrors.ErrLocked
63+
case http.StatusFailedDependency:
64+
return commonerrors.ErrFailed
65+
case http.StatusTooEarly:
66+
return commonerrors.ErrUnexpected
67+
case http.StatusUpgradeRequired:
68+
return commonerrors.ErrUnsupported
69+
case http.StatusPreconditionRequired:
70+
return commonerrors.ErrCondition
71+
case http.StatusTooManyRequests:
72+
return commonerrors.ErrUnavailable
73+
case http.StatusRequestHeaderFieldsTooLarge:
74+
return commonerrors.ErrTooLarge
75+
case http.StatusUnavailableForLegalReasons:
76+
return commonerrors.ErrUnavailable
77+
78+
case http.StatusInternalServerError:
79+
return commonerrors.ErrUnexpected
80+
case http.StatusNotImplemented:
81+
return commonerrors.ErrNotImplemented
82+
case http.StatusBadGateway:
83+
return commonerrors.ErrUnavailable
84+
case http.StatusServiceUnavailable:
85+
return commonerrors.ErrUnavailable
86+
case http.StatusGatewayTimeout:
87+
return commonerrors.ErrTimeout
88+
case http.StatusHTTPVersionNotSupported:
89+
return commonerrors.ErrUnsupported
90+
case http.StatusVariantAlsoNegotiates:
91+
return commonerrors.ErrUnexpected
92+
case http.StatusInsufficientStorage:
93+
return commonerrors.ErrUnexpected
94+
case http.StatusLoopDetected:
95+
return commonerrors.ErrUnexpected
96+
case http.StatusNotExtended:
97+
return commonerrors.ErrUnexpected
98+
case http.StatusNetworkAuthenticationRequired:
99+
return commonerrors.ErrUnauthorised
100+
default:
101+
return commonerrors.ErrUnexpected
102+
}
103+
}

utils/go.mod

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,26 @@ go 1.24
44

55
require (
66
github.com/ARM-software/embedded-development-services-client/client v1.65.0
7-
github.com/ARM-software/golang-utils/utils v1.106.0
7+
github.com/ARM-software/golang-utils/utils v1.117.0
88
github.com/go-faker/faker/v4 v4.6.1
99
github.com/go-logr/logr v1.4.3
1010
github.com/perimeterx/marshmallow v1.1.5
1111
github.com/stretchr/testify v1.10.0
1212
go.uber.org/atomic v1.11.0
1313
go.uber.org/goleak v1.3.0
14-
go.uber.org/mock v0.5.2
14+
go.uber.org/mock v0.6.0
1515
golang.org/x/sync v0.16.0
1616
)
1717

1818
require (
19+
al.essio.dev/pkg/shellescape v1.5.1 // indirect
20+
github.com/DeRuina/timberjack v1.3.5 // indirect
1921
github.com/OneOfOne/xxhash v1.2.8 // indirect
2022
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 // indirect
2123
github.com/avast/retry-go/v4 v4.6.1 // indirect
2224
github.com/bmatcuk/doublestar/v3 v3.0.0 // indirect
2325
github.com/bombsimon/logrusr/v4 v4.1.0 // indirect
26+
github.com/danieljoos/wincred v1.2.2 // indirect
2427
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
2528
github.com/deckarep/golang-set/v2 v2.8.0 // indirect
2629
github.com/djherbis/times v1.6.0 // indirect
@@ -35,11 +38,14 @@ require (
3538
github.com/go-ole/go-ole v1.3.0 // indirect
3639
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
3740
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
41+
github.com/godbus/dbus/v5 v5.1.0 // indirect
3842
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect
3943
github.com/google/cabbie v1.0.5 // indirect
4044
github.com/google/glazier v0.0.0-20250102133340-c90d5bf10f5f // indirect
45+
github.com/hashicorp/errwrap v1.0.0 // indirect
4146
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
4247
github.com/hashicorp/go-hclog v1.6.3 // indirect
48+
github.com/hashicorp/go-multierror v1.1.1 // indirect
4349
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
4450
github.com/iamacarpet/go-win64api v0.0.0-20240507095429-873e84e85847 // indirect
4551
github.com/joho/godotenv v1.5.1 // indirect
@@ -59,28 +65,28 @@ require (
5965
github.com/sagikazarmark/locafero v0.7.0 // indirect
6066
github.com/sasha-s/go-deadlock v0.3.5 // indirect
6167
github.com/scjalliance/comshim v0.0.0-20240712181150-e070933cb68e // indirect
62-
github.com/shirou/gopsutil/v4 v4.25.6 // indirect
68+
github.com/shirou/gopsutil/v4 v4.25.7 // indirect
6369
github.com/sirupsen/logrus v1.9.3 // indirect
6470
github.com/sourcegraph/conc v0.3.0 // indirect
6571
github.com/spaolacci/murmur3 v1.1.0 // indirect
6672
github.com/spf13/afero v1.14.0 // indirect
6773
github.com/spf13/cast v1.7.1 // indirect
68-
github.com/spf13/pflag v1.0.6 // indirect
74+
github.com/spf13/pflag v1.0.7 // indirect
6975
github.com/spf13/viper v1.20.1 // indirect
7076
github.com/subosito/gotenv v1.6.0 // indirect
71-
github.com/tklauser/go-sysconf v0.3.14 // indirect
72-
github.com/tklauser/numcpus v0.9.0 // indirect
77+
github.com/tklauser/go-sysconf v0.3.15 // indirect
78+
github.com/tklauser/numcpus v0.10.0 // indirect
7379
github.com/yusufpapurcu/wmi v1.2.4 // indirect
74-
github.com/zailic/slogr v0.0.2-alpha // indirect
80+
github.com/zalando/go-keyring v0.2.6 // indirect
7581
go.uber.org/multierr v1.11.0 // indirect
7682
go.uber.org/zap v1.27.0 // indirect
77-
golang.org/x/crypto v0.40.0 // indirect
78-
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
79-
golang.org/x/mod v0.26.0 // indirect
83+
golang.org/x/crypto v0.41.0 // indirect
84+
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 // indirect
85+
golang.org/x/mod v0.27.0 // indirect
8086
golang.org/x/oauth2 v0.30.0 // indirect
81-
golang.org/x/sys v0.34.0 // indirect
82-
golang.org/x/text v0.27.0 // indirect
83-
golang.org/x/tools v0.34.0 // indirect
87+
golang.org/x/sys v0.35.0 // indirect
88+
golang.org/x/text v0.28.0 // indirect
89+
golang.org/x/tools v0.36.0 // indirect
8490
gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2 // indirect
8591
gopkg.in/yaml.v3 v3.0.1 // indirect
8692
)

0 commit comments

Comments
 (0)