...

Source file src/cuelabs.dev/go/oci/ociregistry/ociclient/error.go

Documentation: cuelabs.dev/go/oci/ociregistry/ociclient

     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 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  // errorBodySizeLimit holds the maximum number of response bytes aallowed in
    33  // the server's error response. A typical error message is around 200
    34  // bytes. Hence, 8 KiB should be sufficient.
    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  // Code implements [ociregistry.Error.Code].
    67  func (e *wireError) Code() string {
    68  	return e.Code_
    69  }
    70  
    71  // Detail implements [ociregistry.Error.Detail].
    72  func (e *wireError) Detail() any {
    73  	if len(e.Detail_) == 0 {
    74  		return nil
    75  	}
    76  	// TODO do this once only?
    77  	var d any
    78  	json.Unmarshal(e.Detail_, &d)
    79  	return d
    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 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  	// TODO we could do this only once.
    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  // Is makes it possible for users to write `if errors.Is(err, ociregistry.ErrRangeInvalid)`
   104  // even when the error hasn't exactly wrapped that error.
   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  // makeError forms an error from a non-OK response.
   128  func makeError(resp *http.Response) error {
   129  	if resp.Request.Method == "HEAD" {
   130  		// When we've made a HEAD request, we can't see any of
   131  		// the actual error, so we'll have to make up something
   132  		// from the HTTP status.
   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  		// TODO include some of the body in this case?
   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  		// TODO include some part of the body
   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  // isJSONMediaType reports whether the content type implies
   175  // that the content is JSON.
   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  	// Look for +json suffix. See https://tools.ietf.org/html/rfc6838#section-4.2.8
   183  	// We recognize multiple suffixes too (e.g. application/something+json+other)
   184  	// as that seems to be a possibility.
   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