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  }