github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/pkg/couchdb/errors.go (about)

     1  package couchdb
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"net/http"
     8  	"net/url"
     9  	"strconv"
    10  	"strings"
    11  )
    12  
    13  // This file contains error handling code for couchdb request
    14  // Possible errors in connecting to couchdb
    15  // 503 Service Unavailable when the stack cant connect to couchdb or when
    16  // 		 couchdb response is interrupted mid-stream
    17  // 500 When the viper provided configuration does not allow us to properly
    18  // 		 call http.newRequest, ie. wrong couchdbURL config,
    19  //       or when CouchDB has a bug
    20  
    21  // Possible native couchdb errors
    22  // 400 Bad Request : Bad request structure. The error can indicate an error
    23  // 		with the request URL, path or headers. Differences in the supplied MD5
    24  // 		hash and content also trigger this error, as this may indicate message
    25  // 		corruption.
    26  // 401 Unauthorized : The item requested was not available using the supplied
    27  // 		authorization, or authorization was not supplied.
    28  // 		{"error":"unauthorized","reason":"You are not a server admin."}
    29  // 		{"error":"unauthorized","reason":"Name or password is incorrect."}
    30  // 403 Forbidden : The requested item or operation is forbidden.
    31  // 404 Not Found : The requested content could not be found. The content will
    32  // 		include further information, as a JSON object, if available.
    33  // 		**The structure will contain two keys, error and reason.**
    34  //    {"error":"not_found","reason":"deleted"}
    35  //    {"error":"not_found","reason":"missing"}
    36  //    {"error":"not_found","reason":"no_db_file"}
    37  // 405 Resource Not Allowed : A request was made using an invalid HTTP request
    38  // 		type for the URL requested. For example, you have requested a PUT when a
    39  // 		POST is required. Errors of this type can also triggered by invalid URL
    40  // 		strings.
    41  // 406 Not Acceptable : The requested content type is not supported by the
    42  // 		server.
    43  // 409 Conflict : Request resulted in an update conflict.
    44  // 		{"error":"conflict","reason":"Document update conflict."}
    45  // 412 Precondition Failed : The request headers from the client and the
    46  // 		capabilities of the server do not match.
    47  // 415 Bad Content Type : The content types supported, and the content type of
    48  // 		the information being requested or submitted indicate that the content
    49  // 		type is not supported.
    50  // 416 Requested Range Not Satisfiable : The range specified in the request
    51  // 		header cannot be satisfied by the server.
    52  // 417 Expectation Failed : When sending documents in bulk, the bulk load
    53  // 		operation failed.
    54  // 500 Internal Server Error : The request was invalid, either because the
    55  // 		supplied JSON was invalid, or invalid information was supplied as part
    56  // 		of the request.
    57  
    58  // Error represent an error from couchdb
    59  type Error struct {
    60  	StatusCode  int    `json:"status_code"`
    61  	CouchdbJSON []byte `json:"-"`
    62  	Name        string `json:"error"`
    63  	Reason      string `json:"reason"`
    64  	Original    error  `json:"-"`
    65  }
    66  
    67  func (e *Error) Error() string {
    68  	msg := fmt.Sprintf("CouchDB(%s): %s", e.Name, e.Reason)
    69  	if e.Original != nil {
    70  		msg += " - " + e.Original.Error()
    71  	}
    72  	return msg
    73  }
    74  
    75  // JSON returns the json representation of this error
    76  func (e *Error) JSON() map[string]interface{} {
    77  	jsonMap := map[string]interface{}{
    78  		"ok":     false,
    79  		"status": strconv.Itoa(e.StatusCode),
    80  		"error":  e.Name,
    81  		"reason": e.Reason,
    82  	}
    83  	if e.Original != nil {
    84  		jsonMap["original"] = e.Original.Error()
    85  	}
    86  	return jsonMap
    87  }
    88  
    89  // IsCouchError returns whether or not one error from the error
    90  // tree is of type couchdb.Error.
    91  func IsCouchError(err error) (*Error, bool) {
    92  	var couchErr *Error
    93  
    94  	isCouchErr := errors.As(err, &couchErr)
    95  
    96  	return couchErr, isCouchErr
    97  }
    98  
    99  // IsInternalServerError checks if an error from the error tree
   100  // contains a CouchDB error with a 5xx code
   101  func IsInternalServerError(err error) bool {
   102  	var couchErr *Error
   103  
   104  	if ok := errors.As(err, &couchErr); !ok {
   105  		return false
   106  	}
   107  
   108  	return couchErr.StatusCode/100 == 5
   109  }
   110  
   111  // IsNoDatabaseError checks if an error from the error tree
   112  // contains a CouchDB a "no_db_file" error.
   113  func IsNoDatabaseError(err error) bool {
   114  	var couchErr *Error
   115  
   116  	if ok := errors.As(err, &couchErr); !ok {
   117  		return false
   118  	}
   119  
   120  	return couchErr.Reason == "no_db_file" ||
   121  		couchErr.Reason == "Database does not exist."
   122  }
   123  
   124  // IsNotFoundError checks if an error from the error tree
   125  // contains a CouchDB a "not_found" error.
   126  func IsNotFoundError(err error) bool {
   127  	var couchErr *Error
   128  
   129  	if ok := errors.As(err, &couchErr); !ok {
   130  		return false
   131  	}
   132  
   133  	return (couchErr.Name == "not_found" ||
   134  		couchErr.Reason == "no_db_file" ||
   135  		couchErr.Reason == "Database does not exist.")
   136  }
   137  
   138  // IsDeletedError  checks if an error from the error tree
   139  // contains a CouchDB a "not_found" error with the "deleted"
   140  // reason. It means that a document has existed but is
   141  // now deleted.
   142  func IsDeletedError(err error) bool {
   143  	var couchErr *Error
   144  
   145  	if ok := errors.As(err, &couchErr); !ok {
   146  		return false
   147  	}
   148  
   149  	return couchErr.Name == "not_found" && couchErr.Reason == "deleted"
   150  }
   151  
   152  // IsFileExists checks if an error from the error tree
   153  // contains a CouchDB a "file_exists" error.
   154  func IsFileExists(err error) bool {
   155  	var couchErr *Error
   156  
   157  	if ok := errors.As(err, &couchErr); !ok {
   158  		return false
   159  	}
   160  
   161  	return couchErr.Name == "file_exists"
   162  }
   163  
   164  // IsConflictError checks if an error from the error tree
   165  // contains a CouchDB 409 (Conflict) status code.
   166  func IsConflictError(err error) bool {
   167  	var couchErr *Error
   168  
   169  	if ok := errors.As(err, &couchErr); !ok {
   170  		return false
   171  	}
   172  
   173  	return couchErr.StatusCode == http.StatusConflict
   174  }
   175  
   176  // IsNoUsableIndexError checks if an error from the error tree
   177  // contains a "no_usable_index" error.
   178  func IsNoUsableIndexError(err error) bool {
   179  	var couchErr *Error
   180  
   181  	if ok := errors.As(err, &couchErr); !ok {
   182  		return false
   183  	}
   184  
   185  	return couchErr.Name == "no_usable_index"
   186  }
   187  
   188  func isIndexError(err error) bool {
   189  	var couchErr *Error
   190  
   191  	if ok := errors.As(err, &couchErr); !ok {
   192  		return false
   193  	}
   194  
   195  	return strings.Contains(couchErr.Reason, "mango_idx")
   196  }
   197  
   198  func isBadArgError(err error) bool {
   199  	var couchErr *Error
   200  
   201  	if ok := errors.As(err, &couchErr); !ok {
   202  		return false
   203  	}
   204  
   205  	return strings.Contains(couchErr.Reason, "badarg")
   206  }
   207  
   208  func newRequestError(originalError error) error {
   209  	return &Error{
   210  		StatusCode: http.StatusServiceUnavailable,
   211  		Name:       "no_couch",
   212  		Reason:     "could not create a request to the server",
   213  		Original:   cleanURLError(originalError),
   214  	}
   215  }
   216  
   217  func newConnectionError(originalError error) error {
   218  	return &Error{
   219  		StatusCode: http.StatusServiceUnavailable,
   220  		Name:       "no_couch",
   221  		Reason:     "could not create connection with the server",
   222  		Original:   cleanURLError(originalError),
   223  	}
   224  }
   225  
   226  func newIOReadError(originalError error) error {
   227  	return &Error{
   228  		StatusCode: http.StatusServiceUnavailable,
   229  		Name:       "no_couch",
   230  		Reason:     "could not read data from the server",
   231  		Original:   cleanURLError(originalError),
   232  	}
   233  }
   234  
   235  func newDefinedIDError() error {
   236  	return &Error{
   237  		StatusCode: http.StatusBadRequest,
   238  		Name:       "defined_id",
   239  		Reason:     "document _id should be empty",
   240  	}
   241  }
   242  
   243  func newBadIDError(id string) error {
   244  	return &Error{
   245  		StatusCode: http.StatusBadRequest,
   246  		Name:       "bad_id",
   247  		Reason:     fmt.Sprintf("Unsuported couchdb operation %s", id),
   248  	}
   249  }
   250  
   251  func unoptimalError() error {
   252  	return &Error{
   253  		StatusCode: http.StatusBadRequest,
   254  		Name:       "no_index",
   255  		Reason:     "no matching index found, create an index",
   256  	}
   257  }
   258  
   259  func newCouchdbError(statusCode int, couchdbJSON []byte) error {
   260  	err := &Error{
   261  		CouchdbJSON: couchdbJSON,
   262  	}
   263  	parseErr := json.Unmarshal(couchdbJSON, err)
   264  	if parseErr != nil {
   265  		err.Name = "wrong_json"
   266  		err.Reason = parseErr.Error()
   267  	}
   268  	err.StatusCode = statusCode
   269  	return err
   270  }
   271  
   272  func cleanURLError(e error) error {
   273  	if erru, ok := e.(*url.Error); ok {
   274  		u, err := url.Parse(erru.URL)
   275  		if err != nil {
   276  			return erru
   277  		}
   278  		if u.User == nil {
   279  			return erru
   280  		}
   281  		u.User = nil
   282  		return &url.Error{
   283  			Op:  erru.Op,
   284  			URL: u.String(),
   285  			Err: erru.Err,
   286  		}
   287  	}
   288  	return e
   289  }