cuelabs.dev/go/oci/ociregistry@v0.0.0-20240906074133-82eb438dd565/ociclient/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 ociclient
    16  
    17  import (
    18  	"encoding/json"
    19  	"fmt"
    20  	"io"
    21  	"mime"
    22  	"net/http"
    23  	"strings"
    24  
    25  	"cuelabs.dev/go/oci/ociregistry"
    26  )
    27  
    28  // errorBodySizeLimit holds the maximum number of response bytes aallowed in
    29  // the server's error response. A typical error message is around 200
    30  // bytes. Hence, 8 KiB should be sufficient.
    31  const errorBodySizeLimit = 8 * 1024
    32  
    33  // makeError forms an error from a non-OK response.
    34  //
    35  // It reads but does not close resp.Body.
    36  func makeError(resp *http.Response) error {
    37  	var data []byte
    38  	var err error
    39  	if resp.Body != nil {
    40  		data, err = io.ReadAll(io.LimitReader(resp.Body, errorBodySizeLimit+1))
    41  		if err != nil {
    42  			err = fmt.Errorf("cannot read error body: %v", err)
    43  		} else if len(data) > errorBodySizeLimit {
    44  			err = fmt.Errorf("error body too large")
    45  		} else {
    46  			err = makeError1(resp, data)
    47  		}
    48  	}
    49  	// We always include the status code and response in the error.
    50  	return ociregistry.NewHTTPError(err, resp.StatusCode, resp, data)
    51  }
    52  
    53  func makeError1(resp *http.Response, bodyData []byte) error {
    54  	if resp.Request.Method == "HEAD" {
    55  		// When we've made a HEAD request, we can't see any of
    56  		// the actual error, so we'll have to make up something
    57  		// from the HTTP status.
    58  		// TODO would we do better if we interpreted the HTTP status
    59  		// relative to the actual method that was called in order
    60  		// to come up with a more plausible error?
    61  		var err error
    62  		switch resp.StatusCode {
    63  		case http.StatusNotFound:
    64  			err = ociregistry.ErrNameUnknown
    65  		case http.StatusUnauthorized:
    66  			err = ociregistry.ErrUnauthorized
    67  		case http.StatusForbidden:
    68  			err = ociregistry.ErrDenied
    69  		case http.StatusTooManyRequests:
    70  			err = ociregistry.ErrTooManyRequests
    71  		case http.StatusBadRequest:
    72  			err = ociregistry.ErrUnsupported
    73  		default:
    74  			// Our caller will turn this into a non-nil error.
    75  			return nil
    76  		}
    77  		return err
    78  	}
    79  	if ctype := resp.Header.Get("Content-Type"); !isJSONMediaType(ctype) {
    80  		return fmt.Errorf("non-JSON error response %q; body %q", ctype, bodyData)
    81  	}
    82  	var errs ociregistry.WireErrors
    83  	if err := json.Unmarshal(bodyData, &errs); err != nil {
    84  		return fmt.Errorf("%s: malformed error response: %v", resp.Status, err)
    85  	}
    86  	if len(errs.Errors) == 0 {
    87  		return fmt.Errorf("%s: no errors in body (probably a server issue)", resp.Status)
    88  	}
    89  	return &errs
    90  }
    91  
    92  // isJSONMediaType reports whether the content type implies
    93  // that the content is JSON.
    94  func isJSONMediaType(contentType string) bool {
    95  	mediaType, _, _ := mime.ParseMediaType(contentType)
    96  	m := strings.TrimPrefix(mediaType, "application/")
    97  	if len(m) == len(mediaType) {
    98  		return false
    99  	}
   100  	// Look for +json suffix. See https://tools.ietf.org/html/rfc6838#section-4.2.8
   101  	// We recognize multiple suffixes too (e.g. application/something+json+other)
   102  	// as that seems to be a possibility.
   103  	for {
   104  		i := strings.Index(m, "+")
   105  		if i == -1 {
   106  			return m == "json"
   107  		}
   108  		if m[0:i] == "json" {
   109  			return true
   110  		}
   111  		m = m[i+1:]
   112  	}
   113  }