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  }