// Code below is taken from https://github.com/distribution/distribution/blob/a4d9db5a884b70be0c96dd6a7a9dbef4f2798c51/registry/client/errors.go // Copyright 2022 github.com/distribution/distribution authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package docker import ( "encoding/json" "errors" "fmt" "io" "net/http" "slices" "github.com/docker/distribution/registry/api/errcode" ) // errNoErrorsInBody is returned when an HTTP response body parses to an empty // errcode.Errors slice. var errNoErrorsInBody = errors.New("no error details found in HTTP response body") // UnexpectedHTTPStatusError is returned when an unexpected HTTP status is // returned when making a registry api call. type UnexpectedHTTPStatusError struct { // StatusCode code as returned from the server, so callers can // match the exact code to make certain decisions if needed. StatusCode int // status text as displayed in the error message, not exposed as callers should match the number. status string } func (e UnexpectedHTTPStatusError) Error() string { return fmt.Sprintf("received unexpected HTTP status: %s", e.status) } func newUnexpectedHTTPStatusError(resp *http.Response) UnexpectedHTTPStatusError { return UnexpectedHTTPStatusError{ StatusCode: resp.StatusCode, status: resp.Status, } } // unexpectedHTTPResponseError is returned when an expected HTTP status code // is returned, but the content was unexpected and failed to be parsed. type unexpectedHTTPResponseError struct { ParseErr error StatusCode int Response []byte } func (e *unexpectedHTTPResponseError) Error() string { return fmt.Sprintf("error parsing HTTP %d response body: %s: %q", e.StatusCode, e.ParseErr.Error(), string(e.Response)) } func parseHTTPErrorResponse(statusCode int, r io.Reader) error { var errors errcode.Errors body, err := io.ReadAll(r) if err != nil { return err } // For backward compatibility, handle irregularly formatted // messages that contain a "details" field. var detailsErr struct { Details string `json:"details"` } err = json.Unmarshal(body, &detailsErr) if err == nil && detailsErr.Details != "" { switch statusCode { case http.StatusUnauthorized: return errcode.ErrorCodeUnauthorized.WithMessage(detailsErr.Details) case http.StatusTooManyRequests: return errcode.ErrorCodeTooManyRequests.WithMessage(detailsErr.Details) default: return errcode.ErrorCodeUnknown.WithMessage(detailsErr.Details) } } if err := json.Unmarshal(body, &errors); err != nil { return &unexpectedHTTPResponseError{ ParseErr: err, StatusCode: statusCode, Response: body, } } if len(errors) == 0 { // If there was no error specified in the body, return // UnexpectedHTTPResponseError. return &unexpectedHTTPResponseError{ ParseErr: errNoErrorsInBody, StatusCode: statusCode, Response: body, } } return errors } func makeErrorList(err error) []error { if errL, ok := err.(errcode.Errors); ok { return []error(errL) } return []error{err} } func mergeErrors(err1, err2 error) error { return errcode.Errors(append(slices.Clone(makeErrorList(err1)), makeErrorList(err2)...)) } // handleErrorResponse returns error parsed from HTTP response for an // unsuccessful HTTP response code (in the range 400 - 499 inclusive). An // UnexpectedHTTPStatusError returned for response code outside of expected // range. func handleErrorResponse(resp *http.Response) error { switch { case resp.StatusCode == http.StatusUnauthorized: // Check for OAuth errors within the `WWW-Authenticate` header first // See https://tools.ietf.org/html/rfc6750#section-3 for c := range iterateAuthHeader(resp.Header) { if c.Scheme == "bearer" { var err errcode.Error // codes defined at https://tools.ietf.org/html/rfc6750#section-3.1 switch c.Parameters["error"] { case "invalid_token": err.Code = errcode.ErrorCodeUnauthorized case "insufficient_scope": err.Code = errcode.ErrorCodeDenied default: continue } if description := c.Parameters["error_description"]; description != "" { err.Message = description } else { err.Message = err.Code.Message() } return mergeErrors(err, parseHTTPErrorResponse(resp.StatusCode, resp.Body)) } } fallthrough case resp.StatusCode >= 400 && resp.StatusCode < 500: err := parseHTTPErrorResponse(resp.StatusCode, resp.Body) if uErr, ok := err.(*unexpectedHTTPResponseError); ok && resp.StatusCode == 401 { return errcode.ErrorCodeUnauthorized.WithDetail(uErr.Response) } return err } return newUnexpectedHTTPStatusError(resp) }