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 }