cuelabs.dev/go/oci/ociregistry@v0.0.0-20240906074133-82eb438dd565/error.go (about) 1 // Copyright 2023 CUE Labs AG 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 package ociregistry 16 17 import ( 18 "encoding/json" 19 "errors" 20 "fmt" 21 "net/http" 22 "strconv" 23 "strings" 24 "unicode" 25 ) 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 45 // WireErrors is the JSON format used for error responses in 46 // the OCI HTTP API. It should always contain at least one 47 // error. 48 type WireErrors struct { 49 Errors []WireError `json:"errors"` 50 } 51 52 // Unwrap allows [errors.Is] and [errors.As] to 53 // see the errors inside e. 54 func (e *WireErrors) Unwrap() []error { 55 // TODO we could do this only once. 56 errs := make([]error, len(e.Errors)) 57 for i := range e.Errors { 58 errs[i] = &e.Errors[i] 59 } 60 return errs 61 } 62 63 func (e *WireErrors) Error() string { 64 var buf strings.Builder 65 buf.WriteString(e.Errors[0].Error()) 66 for i := range e.Errors[1:] { 67 buf.WriteString("; ") 68 buf.WriteString(e.Errors[i+1].Error()) 69 } 70 return buf.String() 71 } 72 73 // WireError holds a single error in an OCI HTTP response. 74 type WireError struct { 75 Code_ string `json:"code"` 76 Message string `json:"message,omitempty"` 77 // Detail_ holds the JSON detail for the message. 78 // It's assumed to be valid JSON if non-empty. 79 Detail_ json.RawMessage `json:"detail,omitempty"` 80 } 81 82 // Is makes it possible for users to write `if errors.Is(err, ociregistry.ErrBlobUnknown)` 83 // even when the error hasn't exactly wrapped that error. 84 func (e *WireError) Is(err error) bool { 85 var rerr Error 86 return errors.As(err, &rerr) && rerr.Code() == e.Code() 87 } 88 89 // Error implements the [error] interface. 90 func (e *WireError) Error() string { 91 buf := make([]byte, 0, 128) 92 buf = appendErrorCodePrefix(buf, e.Code_) 93 94 if e.Message != "" { 95 buf = append(buf, ": "...) 96 buf = append(buf, e.Message...) 97 } 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) 103 } 104 105 // Code implements [Error.Code]. 106 func (e *WireError) Code() string { 107 return e.Code_ 108 } 109 110 // Detail implements [Error.Detail]. 111 func (e *WireError) Detail() json.RawMessage { 112 return e.Detail_ 113 } 114 115 // NewError returns a new error with the given code, message and detail. 116 func NewError(msg string, code string, detail json.RawMessage) Error { 117 return &WireError{ 118 Code_: code, 119 Message: msg, 120 Detail_: detail, 121 } 122 } 123 124 // Error represents an OCI registry error. The set of codes is defined 125 // in the [distribution specification]. 126 // 127 // [distribution specification]: https://github.com/opencontainers/distribution-spec/blob/main/spec.md#error-codes 128 type Error interface { 129 // error.Error provides the error message. 130 error 131 132 // Code returns the error code. 133 Code() string 134 135 // Detail returns any detail associated with the error, 136 // or nil if there is none. 137 // The caller should not mutate the returned slice. 138 Detail() json.RawMessage 139 } 140 141 // HTTPError is optionally implemented by an error when 142 // the error has originated from an HTTP request 143 // or might be returned from one. 144 type HTTPError interface { 145 error 146 147 // StatusCode returns the HTTP status code of the response. 148 StatusCode() int 149 150 // Response holds the HTTP response that caused the HTTPError to 151 // be created. It will return nil if the error was not created 152 // as a result of an HTTP response. 153 // 154 // The caller should not read the response body or otherwise 155 // change the response (mutation of errors is a Bad Thing). 156 // 157 // Use the ResponseBody method to obtain the body of the 158 // response if needed. 159 Response() *http.Response 160 161 // ResponseBody returns the contents of the response body. It 162 // will return nil if the error was not created as a result of 163 // an HTTP response. 164 // 165 // The caller should not change or append to the returned data. 166 ResponseBody() []byte 167 } 168 169 // NewHTTPError returns an error that wraps err to make an [HTTPError] 170 // that represents the given status code, response and response body. 171 // Both response and body may be nil. 172 // 173 // A shallow copy is made of the response. 174 func NewHTTPError(err error, statusCode int, response *http.Response, body []byte) HTTPError { 175 herr := &httpError{ 176 underlying: err, 177 statusCode: statusCode, 178 } 179 if response != nil { 180 herr.response = ref(*response) 181 herr.response.Body = nil 182 herr.body = body 183 } 184 return herr 185 } 186 187 type httpError struct { 188 underlying error 189 statusCode int 190 response *http.Response 191 body []byte 192 } 193 194 // Unwrap implements the [errors] Unwrap interface. 195 func (e *httpError) Unwrap() error { 196 return e.underlying 197 } 198 199 // Is makes it possible for users to write `if errors.Is(err, ociregistry.ErrRangeInvalid)` 200 // even when the error hasn't exactly wrapped that error. 201 func (e *httpError) Is(err error) bool { 202 switch e.statusCode { 203 case http.StatusRequestedRangeNotSatisfiable: 204 return err == ErrRangeInvalid 205 } 206 return false 207 } 208 209 // Error implements [error.Error]. 210 func (e *httpError) Error() string { 211 buf := make([]byte, 0, 128) 212 buf = appendHTTPStatusPrefix(buf, e.statusCode) 213 if e.underlying != nil { 214 buf = append(buf, ": "...) 215 buf = append(buf, e.underlying.Error()...) 216 } 217 // TODO if underlying is nil, include some portion of e.body in the message? 218 return string(buf) 219 } 220 221 // StatusCode implements [HTTPError.StatusCode]. 222 func (e *httpError) StatusCode() int { 223 return e.statusCode 224 } 225 226 // Response implements [HTTPError.Response]. 227 func (e *httpError) Response() *http.Response { 228 return e.response 229 } 230 231 // ResponseBody implements [HTTPError.ResponseBody]. 232 func (e *httpError) ResponseBody() []byte { 233 return e.body 234 } 235 236 // WriteError marshals the given error as JSON using [MarshalError] and 237 // then writes it to w. It returns the error returned from w.Write. 238 func WriteError(w http.ResponseWriter, err error) error { 239 data, httpStatus := MarshalError(err) 240 w.Header().Set("Content-Type", "application/json") 241 w.WriteHeader(httpStatus) 242 _, err = w.Write(data) 243 return err 244 } 245 246 // MarshalError marshals the given error as JSON according 247 // to the OCI distribution specification. It also returns 248 // the associated HTTP status code, or [http.StatusInternalServerError] 249 // if no specific code can be found. 250 // 251 // If err is or wraps [Error], that code will be used for the "code" 252 // field in the marshaled error. 253 // 254 // If err wraps [HTTPError] and no HTTP status code is known 255 // for the error code, [HTTPError.StatusCode] will be used. 256 func MarshalError(err error) (errorBody []byte, httpStatus int) { 257 var e WireError 258 // TODO perhaps we should iterate through all the 259 // errors instead of just choosing one. 260 // See https://github.com/golang/go/issues/66455 261 var ociErr Error 262 if errors.As(err, &ociErr) { 263 e.Code_ = ociErr.Code() 264 e.Detail_ = ociErr.Detail() 265 } 266 if e.Code_ == "" { 267 // This is contrary to spec, but it's what the Docker registry 268 // does, so it can't be too bad. 269 e.Code_ = "UNKNOWN" 270 } 271 // Use the HTTP status code from the error only when there isn't 272 // one implied from the error code. This means that the HTTP status 273 // is always consistent with the error code, but still allows a registry 274 // to choose custom HTTP status codes for other codes. 275 httpStatus = http.StatusInternalServerError 276 if status, ok := errorStatuses[e.Code_]; ok { 277 httpStatus = status 278 } else { 279 var httpErr HTTPError 280 if errors.As(err, &httpErr) { 281 httpStatus = httpErr.StatusCode() 282 } 283 } 284 // Prevent the message from containing a redundant 285 // error code prefix by stripping it before sending over the 286 // wire. This won't always work, but is enough to prevent 287 // adjacent stuttering of code prefixes when a client 288 // creates a WireError from an error response. 289 e.Message = trimErrorCodePrefix(err, httpStatus, e.Code_) 290 data, err := json.Marshal(WireErrors{ 291 Errors: []WireError{e}, 292 }) 293 if err != nil { 294 panic(fmt.Errorf("cannot marshal error: %v", err)) 295 } 296 return data, httpStatus 297 } 298 299 // trimErrorCodePrefix returns err's string 300 // with any prefix codes added by [HTTPError] 301 // or [WireError] removed. 302 func trimErrorCodePrefix(err error, httpStatus int, errorCode string) string { 303 msg := err.Error() 304 buf := make([]byte, 0, 128) 305 if httpStatus != 0 { 306 buf = appendHTTPStatusPrefix(buf, httpStatus) 307 buf = append(buf, ": "...) 308 msg = strings.TrimPrefix(msg, string(buf)) 309 } 310 if errorCode != "" { 311 buf = buf[:0] 312 buf = appendErrorCodePrefix(buf, errorCode) 313 buf = append(buf, ": "...) 314 msg = strings.TrimPrefix(msg, string(buf)) 315 } 316 return msg 317 } 318 319 // The following values represent the known error codes. 320 var ( 321 ErrBlobUnknown = NewError("blob unknown to registry", "BLOB_UNKNOWN", nil) 322 ErrBlobUploadInvalid = NewError("blob upload invalid", "BLOB_UPLOAD_INVALID", nil) 323 ErrBlobUploadUnknown = NewError("blob upload unknown to registry", "BLOB_UPLOAD_UNKNOWN", nil) 324 ErrDigestInvalid = NewError("provided digest did not match uploaded content", "DIGEST_INVALID", nil) 325 ErrManifestBlobUnknown = NewError("manifest references a manifest or blob unknown to registry", "MANIFEST_BLOB_UNKNOWN", nil) 326 ErrManifestInvalid = NewError("manifest invalid", "MANIFEST_INVALID", nil) 327 ErrManifestUnknown = NewError("manifest unknown to registry", "MANIFEST_UNKNOWN", nil) 328 ErrNameInvalid = NewError("invalid repository name", "NAME_INVALID", nil) 329 ErrNameUnknown = NewError("repository name not known to registry", "NAME_UNKNOWN", nil) 330 ErrSizeInvalid = NewError("provided length did not match content length", "SIZE_INVALID", nil) 331 ErrUnauthorized = NewError("authentication required", "UNAUTHORIZED", nil) 332 ErrDenied = NewError("requested access to the resource is denied", "DENIED", nil) 333 ErrUnsupported = NewError("the operation is unsupported", "UNSUPPORTED", nil) 334 ErrTooManyRequests = NewError("too many requests", "TOOMANYREQUESTS", nil) 335 336 // ErrRangeInvalid allows Interface implementations to reject invalid ranges, 337 // such as a chunked upload PATCH not following the range from a previous PATCH. 338 // ociserver relies on this error to return 416 HTTP status codes. 339 // 340 // It is separate from the Error type since the spec only dictates its HTTP status code, 341 // but does not assign any error code to it. 342 // We borrowed RANGE_INVALID from the Docker registry implementation, a de facto standard. 343 ErrRangeInvalid = NewError("invalid content range", "RANGE_INVALID", nil) 344 ) 345 346 func appendHTTPStatusPrefix(buf []byte, statusCode int) []byte { 347 buf = strconv.AppendInt(buf, int64(statusCode), 10) 348 buf = append(buf, ' ') 349 buf = append(buf, http.StatusText(statusCode)...) 350 return buf 351 } 352 353 func appendErrorCodePrefix(buf []byte, code string) []byte { 354 if code == "" { 355 return append(buf, "(no code)"...) 356 } 357 for _, r := range code { 358 if r == '_' { 359 buf = append(buf, ' ') 360 } else { 361 buf = append(buf, string(unicode.ToLower(r))...) 362 } 363 } 364 return buf 365 } 366 367 func ref[T any](x T) *T { 368 return &x 369 }