github.com/decred/politeia@v1.4.0/politeiawww/legacy/error.go (about)

     1  // Copyright (c) 2021 The Decred developers
     2  // Use of this source code is governed by an ISC
     3  // license that can be found in the LICENSE file.
     4  
     5  package legacy
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"net/http"
    11  	"runtime/debug"
    12  	"strings"
    13  	"time"
    14  
    15  	pd "github.com/decred/politeia/politeiad/api/v1"
    16  	cms "github.com/decred/politeia/politeiawww/api/cms/v1"
    17  	www "github.com/decred/politeia/politeiawww/api/www/v1"
    18  	"github.com/decred/politeia/util"
    19  )
    20  
    21  // RespondWithError returns an HTTP error status to the client. If it's a user
    22  // error, it returns a 4xx HTTP status and the specific user error code. If it's
    23  // an internal server error, it returns 500 and an error code which is also
    24  // outputted to the logs so that it can be correlated later if the user
    25  // files a complaint.
    26  func RespondWithError(w http.ResponseWriter, r *http.Request, userHttpCode int, format string, args ...interface{}) {
    27  	// XXX this function needs to get an error in and a format + args
    28  	// instead of what it is doing now.
    29  	// So inError error, format string, args ...interface{}
    30  	// if err == nil -> internal error using format + args
    31  	// if err != nil -> if defined error -> return defined error + log.Errorf format+args
    32  	// if err != nil -> if !defined error -> return + log.Errorf format+args
    33  
    34  	// Check if the client dropped the connection
    35  	if err := r.Context().Err(); err == context.Canceled {
    36  		log.Infof("%v %v %v %v client aborted connection",
    37  			util.RemoteAddr(r), r.Method, r.URL, r.Proto)
    38  
    39  		// Client dropped the connection. There is no need to
    40  		// respond further.
    41  		return
    42  	}
    43  
    44  	// Check for www user error
    45  	if userErr, ok := args[0].(www.UserError); ok {
    46  		// Error is a www user error. Log it and return a 400.
    47  		if userHttpCode == 0 {
    48  			userHttpCode = http.StatusBadRequest
    49  		}
    50  
    51  		if len(userErr.ErrorContext) == 0 {
    52  			log.Infof("WWW user error: %v %v %v",
    53  				util.RemoteAddr(r), int64(userErr.ErrorCode),
    54  				userErrorStatus(userErr.ErrorCode))
    55  		} else {
    56  			log.Infof("WWW user error: %v %v %v: %v",
    57  				util.RemoteAddr(r), int64(userErr.ErrorCode),
    58  				userErrorStatus(userErr.ErrorCode),
    59  				strings.Join(userErr.ErrorContext, ", "))
    60  		}
    61  
    62  		util.RespondWithJSON(w, userHttpCode,
    63  			www.UserError{
    64  				ErrorCode:    userErr.ErrorCode,
    65  				ErrorContext: userErr.ErrorContext,
    66  			})
    67  		return
    68  	}
    69  
    70  	// Check for politeiad error
    71  	if pdError, ok := args[0].(pdError); ok {
    72  		var (
    73  			pluginID   = pdError.ErrorReply.Plugin
    74  			errCode    = pdError.ErrorReply.ErrorCode
    75  			errContext = pdError.ErrorReply.ErrorContext
    76  		)
    77  
    78  		// Check if the politeiad error corresponds to a www user error
    79  		wwwErrCode := convertWWWErrorStatus(pluginID, errCode)
    80  		if wwwErrCode == www.ErrorStatusInvalid {
    81  			// politeiad error does not correspond to a www user error. Log
    82  			// it and return a 500.
    83  			t := time.Now().Unix()
    84  			if pluginID == "" {
    85  				log.Errorf("%v %v %v %v Internal error %v: error "+
    86  					"code from politeiad: %v", util.RemoteAddr(r), r.Method,
    87  					r.URL, r.Proto, t, errCode)
    88  			} else {
    89  				log.Errorf("%v %v %v %v Internal error %v: error "+
    90  					"code from politeiad plugin %v: %v", util.RemoteAddr(r),
    91  					r.Method, r.URL, r.Proto, t, pluginID, errCode)
    92  			}
    93  
    94  			util.RespondWithJSON(w, http.StatusInternalServerError,
    95  				www.ErrorReply{
    96  					ErrorCode: t,
    97  				})
    98  			return
    99  		}
   100  
   101  		// politeiad error does correspond to a www user error. Log it
   102  		// and return a 400.
   103  		if len(errContext) == 0 {
   104  			log.Infof("WWW user error: %v %v %v",
   105  				util.RemoteAddr(r), int64(wwwErrCode),
   106  				userErrorStatus(wwwErrCode))
   107  		} else {
   108  			log.Infof("WWW user error: %v %v %v: %v",
   109  				util.RemoteAddr(r), int64(wwwErrCode),
   110  				userErrorStatus(wwwErrCode),
   111  				strings.Join(errContext, ", "))
   112  		}
   113  
   114  		util.RespondWithJSON(w, http.StatusBadRequest,
   115  			www.UserError{
   116  				ErrorCode:    wwwErrCode,
   117  				ErrorContext: errContext,
   118  			})
   119  		return
   120  	}
   121  
   122  	// Error is a politeiawww server error. Log it and return a 500.
   123  	t := time.Now().Unix()
   124  	ec := fmt.Sprintf("%v %v %v %v Internal error %v: ", util.RemoteAddr(r),
   125  		r.Method, r.URL, r.Proto, t)
   126  	log.Errorf(ec+format, args...)
   127  	log.Errorf("Stacktrace (NOT A REAL CRASH): %s", debug.Stack())
   128  
   129  	util.RespondWithJSON(w, http.StatusInternalServerError,
   130  		www.ErrorReply{
   131  			ErrorCode: t,
   132  		})
   133  }
   134  
   135  // userErrorStatus retrieves the human readable error message for an error
   136  // status code. The status code can be from either the pi or cms api.
   137  func userErrorStatus(e www.ErrorStatusT) string {
   138  	s, ok := www.ErrorStatus[e]
   139  	if ok {
   140  		return s
   141  	}
   142  	s, ok = cms.ErrorStatus[e]
   143  	if ok {
   144  		return s
   145  	}
   146  	return ""
   147  }
   148  
   149  func convertWWWErrorStatusFromPD(e pd.ErrorStatusT) www.ErrorStatusT {
   150  	switch e {
   151  	case pd.ErrorStatusInvalidRequestPayload:
   152  		// Intentionally omitted because this indicates a politeiawww
   153  		// server error so a ErrorStatusInvalid should be returned.
   154  	case pd.ErrorStatusInvalidChallenge:
   155  		// Intentionally omitted because this indicates a politeiawww
   156  		// server error so a ErrorStatusInvalid should be returned.
   157  	case pd.ErrorStatusInvalidFilename:
   158  		return www.ErrorStatusInvalidFilename
   159  	case pd.ErrorStatusInvalidFileDigest:
   160  		return www.ErrorStatusInvalidFileDigest
   161  	case pd.ErrorStatusInvalidBase64:
   162  		return www.ErrorStatusInvalidBase64
   163  	case pd.ErrorStatusInvalidMIMEType:
   164  		return www.ErrorStatusInvalidMIMEType
   165  	case pd.ErrorStatusUnsupportedMIMEType:
   166  		return www.ErrorStatusUnsupportedMIMEType
   167  	case pd.ErrorStatusInvalidRecordStatusTransition:
   168  		return www.ErrorStatusInvalidPropStatusTransition
   169  	}
   170  	return www.ErrorStatusInvalid
   171  }
   172  
   173  func convertWWWErrorStatus(pluginID string, errCode int) www.ErrorStatusT {
   174  	switch pluginID {
   175  	case "":
   176  		// politeiad API
   177  		e := pd.ErrorStatusT(errCode)
   178  		return convertWWWErrorStatusFromPD(e)
   179  	}
   180  
   181  	// No corresponding www error status found
   182  	return www.ErrorStatusInvalid
   183  }