github.com/treeverse/lakefs@v1.24.1-0.20240520134607-95648127bfb0/pkg/api/helpers/errors.go (about) 1 package helpers 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "io" 8 "net/http" 9 "reflect" 10 "strings" 11 12 "github.com/treeverse/lakefs/pkg/api/apigen" 13 ) 14 15 var ( 16 // ErrUnsupportedProtocol is the error returned when lakeFS server requests the client 17 // use a protocol it does not know about. Until recompiled to use a newer client, 18 // upload the object using through the lakeFS server. 19 ErrUnsupportedProtocol = errors.New("unsupported protocol") 20 21 // ErrRequestFailed is an error returned for failing lakeFS server replies. 22 ErrRequestFailed = errors.New("request failed") 23 ErrConflict = errors.New("conflict") 24 ) 25 26 const minHTTPErrorStatusCode = 400 27 28 // isOK returns true if statusCode is an OK HTTP status code: 0-399. 29 func isOK(statusCode int) bool { 30 return statusCode < minHTTPErrorStatusCode 31 } 32 33 // APIFields are fields to use to format an HTTP error response that can be 34 // shown to the user. 35 type APIFields struct { 36 StatusCode int 37 Status string 38 Message string 39 } 40 41 // CallFailedError is an error performing the HTTP request itself formatted 42 // to be shown to a user. It does _not_ update its message when wrapped so 43 // usually should not be wrapped. 44 type CallFailedError struct { 45 Err error 46 Message string 47 } 48 49 func (e CallFailedError) Error() string { 50 wrapped := "" 51 if e.Err != nil { 52 wrapped = ": " + e.Err.Error() 53 } 54 return fmt.Sprintf("[%s]%s", e.Message, wrapped) 55 } 56 57 func (e CallFailedError) Unwrap() error { 58 return e.Err 59 } 60 61 // UserVisibleAPIError is an HTTP error response formatted to be shown to a 62 // user. It does _not_ update its message when wrapped so usually should 63 // not be wrapped. 64 type UserVisibleAPIError struct { 65 APIFields 66 Err error 67 } 68 69 // space stringifies non-nil elements from s... and returns all the 70 // non-empty resulting strings joined with spaces. 71 func spaced(s ...interface{}) string { 72 ret := make([]string, 0, len(s)) 73 for _, t := range s { 74 if t != nil { 75 r := fmt.Sprint(t) 76 if r != "" { 77 ret = append(ret, r) 78 } 79 } 80 } 81 return strings.Join(ret, " ") 82 } 83 84 func (e UserVisibleAPIError) Error() string { 85 message := spaced(e.Message, e.Err.Error()) 86 if message != "" { 87 message = ": " + message 88 } 89 return fmt.Sprintf("[%s]%s", e.Status, message) 90 } 91 92 func (e UserVisibleAPIError) Unwrap() error { 93 return e.Err 94 } 95 96 // ResponseAsError returns a UserVisibleAPIError wrapping an ErrRequestFailed 97 // wrapping a response from the server. It searches for a non-nil 98 // unsuccessful HTTPResponse field and uses its message, along with a Body 99 // that it assumes is an api.Error. 100 func ResponseAsError(response interface{}) error { 101 if response == nil { 102 return nil 103 } 104 if httpResponse, ok := response.(*http.Response); ok { 105 return HTTPResponseAsError(httpResponse) 106 } 107 r := reflect.Indirect(reflect.ValueOf(response)) 108 if !r.IsValid() || r.Kind() != reflect.Struct { 109 return CallFailedError{ 110 Message: fmt.Sprintf("bad type %s: must reference a struct", r.Type().Name()), 111 Err: ErrRequestFailed, 112 } 113 } 114 var ok bool 115 f := r.FieldByName("HTTPResponse") 116 if !f.IsValid() { 117 return fmt.Errorf("[no HTTPResponse]: %w", ErrRequestFailed) 118 } 119 httpResponse, ok := f.Interface().(*http.Response) 120 if !ok { 121 return fmt.Errorf("%w: no HTTPResponse", ErrRequestFailed) 122 } 123 if httpResponse == nil || isOK(httpResponse.StatusCode) { 124 return nil 125 } 126 127 statusCode := httpResponse.StatusCode 128 statusText := httpResponse.Status 129 if statusText == "" { 130 statusText = http.StatusText(statusCode) 131 } 132 133 var message string 134 f = r.FieldByName("Body") 135 if f.IsValid() && f.Type().Kind() == reflect.Slice && f.Type().Elem().Kind() == reflect.Uint8 { 136 body := f.Bytes() 137 var apiError apigen.Error 138 if json.Unmarshal(body, &apiError) == nil && apiError.Message != "" { 139 message = apiError.Message 140 } 141 } 142 143 return UserVisibleAPIError{ 144 Err: ErrRequestFailed, 145 APIFields: APIFields{ 146 StatusCode: statusCode, 147 Status: statusText, 148 Message: message, 149 }, 150 } 151 } 152 153 func HTTPResponseAsError(httpResponse *http.Response) error { 154 if httpResponse == nil || isOK(httpResponse.StatusCode) { 155 return nil 156 } 157 statusCode := httpResponse.StatusCode 158 statusText := httpResponse.Status 159 if statusText == "" { 160 statusText = http.StatusText(statusCode) 161 } 162 var message string 163 body, err := io.ReadAll(httpResponse.Body) 164 if err == nil { 165 var apiError apigen.Error 166 if json.Unmarshal(body, &apiError) == nil && apiError.Message != "" { 167 message = apiError.Message 168 } 169 } 170 return UserVisibleAPIError{ 171 Err: ErrRequestFailed, 172 APIFields: APIFields{ 173 StatusCode: statusCode, 174 Status: statusText, 175 Message: message, 176 }, 177 } 178 }