Browse code

client: Use containerd errdefs to convert http errors

Previously, we were using our own `FromStatusCode` function to map HTTP
status codes to Docker error types. Switch to the containerd code.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>

Paweł Gronowski authored on 2025/05/19 21:16:41
Showing 7 changed files
... ...
@@ -4,8 +4,10 @@ import (
4 4
 	"context"
5 5
 	"errors"
6 6
 	"fmt"
7
+	"net/http"
7 8
 
8 9
 	cerrdefs "github.com/containerd/errdefs"
10
+	"github.com/containerd/errdefs/pkg/errhttp"
9 11
 	"github.com/docker/docker/api/types/versions"
10 12
 )
11 13
 
... ...
@@ -85,3 +87,43 @@ func (cli *Client) NewVersionError(ctx context.Context, APIrequired, feature str
85 85
 	}
86 86
 	return nil
87 87
 }
88
+
89
+type httpError struct {
90
+	err    error
91
+	errdef error
92
+}
93
+
94
+func (e *httpError) Error() string {
95
+	return e.err.Error()
96
+}
97
+
98
+func (e *httpError) Unwrap() error {
99
+	return e.err
100
+}
101
+
102
+func (e *httpError) Is(target error) bool {
103
+	return errors.Is(e.errdef, target)
104
+}
105
+
106
+// httpErrorFromStatusCode creates an errdef error, based on the provided HTTP status-code
107
+func httpErrorFromStatusCode(err error, statusCode int) error {
108
+	if err == nil {
109
+		return nil
110
+	}
111
+	base := errhttp.ToNative(statusCode)
112
+	if base != nil {
113
+		return &httpError{err: err, errdef: base}
114
+	}
115
+
116
+	switch {
117
+	case statusCode >= http.StatusOK && statusCode < http.StatusBadRequest:
118
+		// it's a client error
119
+		return err
120
+	case statusCode >= http.StatusBadRequest && statusCode < http.StatusInternalServerError:
121
+		return &httpError{err: err, errdef: cerrdefs.ErrInvalidArgument}
122
+	case statusCode >= http.StatusInternalServerError && statusCode < 600:
123
+		return &httpError{err: err, errdef: cerrdefs.ErrInternal}
124
+	default:
125
+		return &httpError{err: err, errdef: cerrdefs.ErrUnknown}
126
+	}
127
+}
... ...
@@ -15,7 +15,6 @@ import (
15 15
 
16 16
 	"github.com/docker/docker/api/types"
17 17
 	"github.com/docker/docker/api/types/versions"
18
-	"github.com/docker/docker/errdefs"
19 18
 	"github.com/pkg/errors"
20 19
 )
21 20
 
... ...
@@ -197,7 +196,7 @@ func (cli *Client) checkResponseErr(serverResp *http.Response) (retErr error) {
197 197
 		return nil
198 198
 	}
199 199
 	defer func() {
200
-		retErr = errdefs.FromStatusCode(retErr, serverResp.StatusCode)
200
+		retErr = httpErrorFromStatusCode(retErr, serverResp.StatusCode)
201 201
 	}()
202 202
 
203 203
 	var body []byte
... ...
@@ -5,6 +5,8 @@ import (
5 5
 )
6 6
 
7 7
 // FromStatusCode creates an errdef error, based on the provided HTTP status-code
8
+//
9
+// Deprecated: Use [cerrdefs.ToNative] instead
8 10
 func FromStatusCode(err error, statusCode int) error {
9 11
 	if err == nil {
10 12
 		return nil
... ...
@@ -83,6 +83,7 @@ func TestFromStatusCode(t *testing.T) {
83 83
 
84 84
 	for _, tc := range testCases {
85 85
 		t.Run(http.StatusText(tc.status), func(t *testing.T) {
86
+			//nolint:staticcheck // ignore SA1019: FromStatusCode is deprecated
86 87
 			err := FromStatusCode(tc.err, tc.status)
87 88
 			if !tc.check(err) {
88 89
 				t.Errorf("unexpected error-type %T", err)
... ...
@@ -30,6 +30,7 @@ require (
30 30
 	github.com/containerd/containerd/v2 v2.0.5
31 31
 	github.com/containerd/continuity v0.4.5
32 32
 	github.com/containerd/errdefs v1.0.0
33
+	github.com/containerd/errdefs/pkg v0.3.0
33 34
 	github.com/containerd/fifo v1.1.0
34 35
 	github.com/containerd/log v0.1.0
35 36
 	github.com/containerd/platforms v1.0.0-rc.1
... ...
@@ -148,7 +149,6 @@ require (
148 148
 	github.com/container-storage-interface/spec v1.5.0 // indirect
149 149
 	github.com/containerd/accelerated-container-image v1.3.0 // indirect
150 150
 	github.com/containerd/console v1.0.4 // indirect
151
-	github.com/containerd/errdefs/pkg v0.3.0 // indirect
152 151
 	github.com/containerd/go-cni v1.1.12 // indirect
153 152
 	github.com/containerd/go-runc v1.1.0 // indirect
154 153
 	github.com/containerd/nydus-snapshotter v0.15.0 // indirect
155 154
new file mode 100644
... ...
@@ -0,0 +1,96 @@
0
+/*
1
+   Copyright The containerd Authors.
2
+
3
+   Licensed under the Apache License, Version 2.0 (the "License");
4
+   you may not use this file except in compliance with the License.
5
+   You may obtain a copy of the License at
6
+
7
+       http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+   Unless required by applicable law or agreed to in writing, software
10
+   distributed under the License is distributed on an "AS IS" BASIS,
11
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+   See the License for the specific language governing permissions and
13
+   limitations under the License.
14
+*/
15
+
16
+// Package errhttp provides utility functions for translating errors to
17
+// and from a HTTP context.
18
+//
19
+// The functions ToHTTP and ToNative can be used to map server-side and
20
+// client-side errors to the correct types.
21
+package errhttp
22
+
23
+import (
24
+	"errors"
25
+	"net/http"
26
+
27
+	"github.com/containerd/errdefs"
28
+	"github.com/containerd/errdefs/pkg/internal/cause"
29
+)
30
+
31
+// ToHTTP returns the best status code for the given error
32
+func ToHTTP(err error) int {
33
+	switch {
34
+	case errdefs.IsNotFound(err):
35
+		return http.StatusNotFound
36
+	case errdefs.IsInvalidArgument(err):
37
+		return http.StatusBadRequest
38
+	case errdefs.IsConflict(err):
39
+		return http.StatusConflict
40
+	case errdefs.IsNotModified(err):
41
+		return http.StatusNotModified
42
+	case errdefs.IsFailedPrecondition(err):
43
+		return http.StatusPreconditionFailed
44
+	case errdefs.IsUnauthorized(err):
45
+		return http.StatusUnauthorized
46
+	case errdefs.IsPermissionDenied(err):
47
+		return http.StatusForbidden
48
+	case errdefs.IsResourceExhausted(err):
49
+		return http.StatusTooManyRequests
50
+	case errdefs.IsInternal(err):
51
+		return http.StatusInternalServerError
52
+	case errdefs.IsNotImplemented(err):
53
+		return http.StatusNotImplemented
54
+	case errdefs.IsUnavailable(err):
55
+		return http.StatusServiceUnavailable
56
+	case errdefs.IsUnknown(err):
57
+		var unexpected cause.ErrUnexpectedStatus
58
+		if errors.As(err, &unexpected) && unexpected.Status >= 200 && unexpected.Status < 600 {
59
+			return unexpected.Status
60
+		}
61
+		return http.StatusInternalServerError
62
+	default:
63
+		return http.StatusInternalServerError
64
+	}
65
+}
66
+
67
+// ToNative returns the error best matching the HTTP status code
68
+func ToNative(statusCode int) error {
69
+	switch statusCode {
70
+	case http.StatusNotFound:
71
+		return errdefs.ErrNotFound
72
+	case http.StatusBadRequest:
73
+		return errdefs.ErrInvalidArgument
74
+	case http.StatusConflict:
75
+		return errdefs.ErrConflict
76
+	case http.StatusPreconditionFailed:
77
+		return errdefs.ErrFailedPrecondition
78
+	case http.StatusUnauthorized:
79
+		return errdefs.ErrUnauthenticated
80
+	case http.StatusForbidden:
81
+		return errdefs.ErrPermissionDenied
82
+	case http.StatusNotModified:
83
+		return errdefs.ErrNotModified
84
+	case http.StatusTooManyRequests:
85
+		return errdefs.ErrResourceExhausted
86
+	case http.StatusInternalServerError:
87
+		return errdefs.ErrInternal
88
+	case http.StatusNotImplemented:
89
+		return errdefs.ErrNotImplemented
90
+	case http.StatusServiceUnavailable:
91
+		return errdefs.ErrUnavailable
92
+	default:
93
+		return cause.ErrUnexpectedStatus{Status: statusCode}
94
+	}
95
+}
... ...
@@ -420,6 +420,7 @@ github.com/containerd/errdefs
420 420
 # github.com/containerd/errdefs/pkg v0.3.0
421 421
 ## explicit; go 1.22
422 422
 github.com/containerd/errdefs/pkg/errgrpc
423
+github.com/containerd/errdefs/pkg/errhttp
423 424
 github.com/containerd/errdefs/pkg/internal/cause
424 425
 github.com/containerd/errdefs/pkg/internal/types
425 426
 # github.com/containerd/fifo v1.1.0