...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package ociclient
16
17 import (
18 "bytes"
19 "encoding/json"
20 "errors"
21 "fmt"
22 "io"
23 "mime"
24 "net/http"
25 "strconv"
26 "strings"
27 "unicode"
28
29 "cuelabs.dev/go/oci/ociregistry"
30 )
31
32
33
34
35 const errorBodySizeLimit = 8 * 1024
36
37 type wireError struct {
38 Code_ string `json:"code"`
39 Message string `json:"message,omitempty"`
40 Detail_ json.RawMessage `json:"detail,omitempty"`
41 }
42
43 func (e *wireError) Error() string {
44 var buf strings.Builder
45 for _, r := range e.Code_ {
46 if r == '_' {
47 buf.WriteByte(' ')
48 } else {
49 buf.WriteRune(unicode.ToLower(r))
50 }
51 }
52 if buf.Len() == 0 {
53 buf.WriteString("(no code)")
54 }
55 if e.Message != "" {
56 buf.WriteString(": ")
57 buf.WriteString(e.Message)
58 }
59 if len(e.Detail_) != 0 && !bytes.Equal(e.Detail_, []byte("null")) {
60 buf.WriteString("; detail: ")
61 buf.Write(e.Detail_)
62 }
63 return buf.String()
64 }
65
66
67 func (e *wireError) Code() string {
68 return e.Code_
69 }
70
71
72 func (e *wireError) Detail() any {
73 if len(e.Detail_) == 0 {
74 return nil
75 }
76
77 var d any
78 json.Unmarshal(e.Detail_, &d)
79 return d
80 }
81
82
83
84 func (e *wireError) Is(err error) bool {
85 var rerr ociregistry.Error
86 return errors.As(err, &rerr) && rerr.Code() == e.Code()
87 }
88
89 type wireErrors struct {
90 httpStatusCode int
91 Errors []wireError `json:"errors"`
92 }
93
94 func (e *wireErrors) Unwrap() []error {
95
96 errs := make([]error, len(e.Errors))
97 for i := range e.Errors {
98 errs[i] = &e.Errors[i]
99 }
100 return errs
101 }
102
103
104
105 func (e *wireErrors) Is(err error) bool {
106 switch e.httpStatusCode {
107 case http.StatusRequestedRangeNotSatisfiable:
108 return err == ociregistry.ErrRangeInvalid
109 }
110 return false
111 }
112
113 func (e *wireErrors) Error() string {
114 var buf strings.Builder
115 buf.WriteString(strconv.Itoa(e.httpStatusCode))
116 buf.WriteString(" ")
117 buf.WriteString(http.StatusText(e.httpStatusCode))
118 buf.WriteString(": ")
119 buf.WriteString(e.Errors[0].Error())
120 for i := range e.Errors[1:] {
121 buf.WriteString("; ")
122 buf.WriteString(e.Errors[i+1].Error())
123 }
124 return buf.String()
125 }
126
127
128 func makeError(resp *http.Response) error {
129 if resp.Request.Method == "HEAD" {
130
131
132
133 var err error
134 switch resp.StatusCode {
135 case http.StatusNotFound:
136 err = ociregistry.ErrNameUnknown
137 case http.StatusUnauthorized:
138 err = ociregistry.ErrUnauthorized
139 case http.StatusForbidden:
140 err = ociregistry.ErrDenied
141 case http.StatusTooManyRequests:
142 err = ociregistry.ErrTooManyRequests
143 case http.StatusBadRequest:
144 err = ociregistry.ErrUnsupported
145 default:
146 return fmt.Errorf("error response: %v", resp.Status)
147 }
148 return fmt.Errorf("error response: %v: %w", resp.Status, err)
149 }
150 if !isJSONMediaType(resp.Header.Get("Content-Type")) || resp.Request.Method == "HEAD" {
151
152 data, _ := io.ReadAll(resp.Body)
153 return fmt.Errorf("error response: %v; body: %q", resp.Status, data)
154 }
155 data, err := io.ReadAll(io.LimitReader(resp.Body, errorBodySizeLimit+1))
156 if err != nil {
157 return fmt.Errorf("%s: cannot read error body: %v", resp.Status, err)
158 }
159 if len(data) > errorBodySizeLimit {
160
161 return fmt.Errorf("error body too large")
162 }
163 var errs wireErrors
164 if err := json.Unmarshal(data, &errs); err != nil {
165 return fmt.Errorf("%s: malformed error response: %v", resp.Status, err)
166 }
167 if len(errs.Errors) == 0 {
168 return fmt.Errorf("%s: no errors in body (probably a server issue)", resp.Status)
169 }
170 errs.httpStatusCode = resp.StatusCode
171 return &errs
172 }
173
174
175
176 func isJSONMediaType(contentType string) bool {
177 mediaType, _, _ := mime.ParseMediaType(contentType)
178 m := strings.TrimPrefix(mediaType, "application/")
179 if len(m) == len(mediaType) {
180 return false
181 }
182
183
184
185 for {
186 i := strings.Index(m, "+")
187 if i == -1 {
188 return m == "json"
189 }
190 if m[0:i] == "json" {
191 return true
192 }
193 m = m[i+1:]
194 }
195 }
196
View as plain text