15
15
package ociregistry
16
16
17
17
import (
18
- "bytes"
19
18
"encoding/json"
20
19
"errors"
20
+ "fmt"
21
21
"net/http"
22
22
"strconv"
23
23
"strings"
24
24
"unicode"
25
25
)
26
26
27
+ var errorStatuses = map [string ]int {
28
+ ErrBlobUnknown .Code (): http .StatusNotFound ,
29
+ ErrBlobUploadInvalid .Code (): http .StatusRequestedRangeNotSatisfiable ,
30
+ ErrBlobUploadUnknown .Code (): http .StatusNotFound ,
31
+ ErrDigestInvalid .Code (): http .StatusBadRequest ,
32
+ ErrManifestBlobUnknown .Code (): http .StatusNotFound ,
33
+ ErrManifestInvalid .Code (): http .StatusBadRequest ,
34
+ ErrManifestUnknown .Code (): http .StatusNotFound ,
35
+ ErrNameInvalid .Code (): http .StatusBadRequest ,
36
+ ErrNameUnknown .Code (): http .StatusNotFound ,
37
+ ErrSizeInvalid .Code (): http .StatusBadRequest ,
38
+ ErrUnauthorized .Code (): http .StatusUnauthorized ,
39
+ ErrDenied .Code (): http .StatusForbidden ,
40
+ ErrUnsupported .Code (): http .StatusBadRequest ,
41
+ ErrTooManyRequests .Code (): http .StatusTooManyRequests ,
42
+ ErrRangeInvalid .Code (): http .StatusRequestedRangeNotSatisfiable ,
43
+ }
44
+
27
45
// WireErrors is the JSON format used for error responses in
28
46
// the OCI HTTP API. It should always contain at least one
29
47
// error.
@@ -70,26 +88,18 @@ func (e *WireError) Is(err error) bool {
70
88
71
89
// Error implements the [error] interface.
72
90
func (e * WireError ) Error () string {
73
- var buf strings.Builder
74
- for _ , r := range e .Code_ {
75
- if r == '_' {
76
- buf .WriteByte (' ' )
77
- } else {
78
- buf .WriteRune (unicode .ToLower (r ))
79
- }
80
- }
81
- if buf .Len () == 0 {
82
- buf .WriteString ("(no code)" )
83
- }
91
+ buf := make ([]byte , 0 , 128 )
92
+ buf = appendErrorCodePrefix (buf , e .Code_ )
93
+
84
94
if e .Message != "" {
85
- buf . WriteString ( ": " )
86
- buf . WriteString ( e .Message )
95
+ buf = append ( buf , ": " ... )
96
+ buf = append ( buf , e .Message ... )
87
97
}
88
- if len ( e . Detail_ ) != 0 && ! bytes . Equal ( e . Detail_ , [] byte ( "null" )) {
89
- buf . WriteString ( "; detail: " )
90
- buf . Write ( e . Detail_ )
91
- }
92
- return buf . String ( )
98
+ // TODO: it would be nice to have some way to surface the detail
99
+ // in a message, but it's awkward to do so here because we don't
100
+ // really want the detail to be duplicated in the "message"
101
+ // and "detail" fields.
102
+ return string ( buf )
93
103
}
94
104
95
105
// Code implements [Error.Code].
@@ -198,16 +208,14 @@ func (e *httpError) Is(err error) bool {
198
208
199
209
// Error implements [error.Error].
200
210
func (e * httpError ) Error () string {
201
- var buf strings.Builder
202
- buf .WriteString (strconv .Itoa (e .statusCode ))
203
- buf .WriteString (" " )
204
- buf .WriteString (http .StatusText (e .statusCode ))
211
+ buf := make ([]byte , 0 , 128 )
212
+ buf = appendHTTPStatusPrefix (buf , e .statusCode )
205
213
if e .underlying != nil {
206
- buf . WriteString ( ": " )
207
- buf . WriteString ( e .underlying .Error ())
214
+ buf = append ( buf , ": " ... )
215
+ buf = append ( buf , e .underlying .Error ()... )
208
216
}
209
217
// TODO if underlying is nil, include some portion of e.body in the message?
210
- return buf . String ( )
218
+ return string ( buf )
211
219
}
212
220
213
221
// StatusCode implements [HTTPError.StatusCode].
@@ -225,6 +233,79 @@ func (e *httpError) ResponseBody() []byte {
225
233
return e .body
226
234
}
227
235
236
+ // MarshalError marshals the given error as JSON according
237
+ // to the OCI distribution specification. It also returns
238
+ // the associated HTTP status code, or [http.StatusInternalServerError]
239
+ // if no specific code can be found.
240
+ //
241
+ // If err is or wraps [Error], that code will be used for the "code"
242
+ // field in the marshaled error.
243
+ //
244
+ // If err wraps [HTTPError] and no HTTP status code is known
245
+ // for the error code, [HTTPError.StatusCode] will be used.
246
+ func MarshalError (err error ) (errorBody []byte , httpStatus int ) {
247
+ var e WireError
248
+ // TODO perhaps we should iterate through all the
249
+ // errors instead of just choosing one.
250
+ // See https://github.com/golang/go/issues/66455
251
+ var ociErr Error
252
+ if errors .As (err , & ociErr ) {
253
+ e .Code_ = ociErr .Code ()
254
+ e .Detail_ = ociErr .Detail ()
255
+ }
256
+ if e .Code_ == "" {
257
+ // This is contrary to spec, but it's what the Docker registry
258
+ // does, so it can't be too bad.
259
+ e .Code_ = "UNKNOWN"
260
+ }
261
+ // Use the HTTP status code from the error only when there isn't
262
+ // one implied from the error code. This means that the HTTP status
263
+ // is always consistent with the error code, but still allows a registry
264
+ // to choose custom HTTP status codes for other codes.
265
+ httpStatus = http .StatusInternalServerError
266
+ if status , ok := errorStatuses [e .Code_ ]; ok {
267
+ httpStatus = status
268
+ } else {
269
+ var httpErr HTTPError
270
+ if errors .As (err , & httpErr ) {
271
+ httpStatus = httpErr .StatusCode ()
272
+ }
273
+ }
274
+ // Prevent the message from containing a redundant
275
+ // error code prefix by stripping it before sending over the
276
+ // wire. This won't always work, but is enough to prevent
277
+ // adjacent stuttering of code prefixes when a client
278
+ // creates a WireError from an error response.
279
+ e .Message = trimErrorCodePrefix (err , httpStatus , e .Code_ )
280
+ data , err := json .Marshal (WireErrors {
281
+ Errors : []WireError {e },
282
+ })
283
+ if err != nil {
284
+ panic (fmt .Errorf ("cannot marshal error: %v" , err ))
285
+ }
286
+ return data , httpStatus
287
+ }
288
+
289
+ // trimErrorCodePrefix returns err's string
290
+ // with any prefix codes added by [HTTPError]
291
+ // or [WireError] removed.
292
+ func trimErrorCodePrefix (err error , httpStatus int , errorCode string ) string {
293
+ msg := err .Error ()
294
+ buf := make ([]byte , 0 , 128 )
295
+ if httpStatus != 0 {
296
+ buf = appendHTTPStatusPrefix (buf , httpStatus )
297
+ buf = append (buf , ": " ... )
298
+ msg = strings .TrimPrefix (msg , string (buf ))
299
+ }
300
+ if errorCode != "" {
301
+ buf = buf [:0 ]
302
+ buf = appendErrorCodePrefix (buf , errorCode )
303
+ buf = append (buf , ": " ... )
304
+ msg = strings .TrimPrefix (msg , string (buf ))
305
+ }
306
+ return msg
307
+ }
308
+
228
309
// The following values represent the known error codes.
229
310
var (
230
311
ErrBlobUnknown = NewError ("blob unknown to registry" , "BLOB_UNKNOWN" , nil )
@@ -252,6 +333,27 @@ var (
252
333
ErrRangeInvalid = NewError ("invalid content range" , "RANGE_INVALID" , nil )
253
334
)
254
335
336
+ func appendHTTPStatusPrefix (buf []byte , statusCode int ) []byte {
337
+ buf = strconv .AppendInt (buf , int64 (statusCode ), 10 )
338
+ buf = append (buf , ' ' )
339
+ buf = append (buf , http .StatusText (statusCode )... )
340
+ return buf
341
+ }
342
+
343
+ func appendErrorCodePrefix (buf []byte , code string ) []byte {
344
+ if code == "" {
345
+ return append (buf , "(no code)" ... )
346
+ }
347
+ for _ , r := range code {
348
+ if r == '_' {
349
+ buf = append (buf , ' ' )
350
+ } else {
351
+ buf = append (buf , string (unicode .ToLower (r ))... )
352
+ }
353
+ }
354
+ return buf
355
+ }
356
+
255
357
func ref [T any ](x T ) * T {
256
358
return & x
257
359
}
0 commit comments