package api_responses import ( "encoding/json" "fmt" "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/date_utils" "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/errors" "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/map_utils" "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/string_utils" "net/http" "regexp" "strconv" "strings" "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/utils" "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/logs" "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/responses" "github.com/go-pg/pg/v10" "github.com/aws/aws-lambda-go/events" ) var ContentTypeJSONHeader = map[string]string{"Content-Type": "application/json"} type errorMsg struct { Message string `json:"message"` Error string `json:"error,omitempty"` } // ServerError logs any error to os.Stderr and returns 500 // Internal Server Error response that the AWS API Gateway understands. func ServerError(err error, msg string) (events.APIGatewayProxyResponse, error) { return Error(err, msg, http.StatusInternalServerError) } func Error(err error, msg string, statusCode int) (events.APIGatewayProxyResponse, error) { logs.ErrorWithFields(map[string]interface{}{ "type": "Server error", "message": msg, "code": statusCode, }, err) serverError := errorMsg{ Message: msg, Error: err.Error(), } bodyBytes, err := json.Marshal(serverError) if err != nil { return events.APIGatewayProxyResponse{ StatusCode: statusCode, Headers: map_utils.MergeMaps(utils.CorsHeaders(), responses.ContentTypeJSONHeader), Body: "{ \"error\": \"" + http.StatusText(http.StatusInternalServerError) + "\"}", }, nil } return events.APIGatewayProxyResponse{ StatusCode: statusCode, Headers: map_utils.MergeMaps(utils.CorsHeaders(), responses.ContentTypeJSONHeader), Body: string(bodyBytes), }, errors.Error(msg) } func DatabaseServerErrorNew(err error, msg string) error { statusCode := StatusCodeFromSQLError(err) errorString := err.Error() if dbError := ErrorFromDBError(err); dbError != "" { errorString = dbError if strings.HasSuffix(msg, ".") { // Remove trailing full stop before adding dbError. msg = strings.TrimSuffix(msg, ".") } msg = msg + ": " + dbError } if statusCode == http.StatusNotFound { logs.Info("Database error: " + msg + ". Code: " + strconv.Itoa(statusCode)) } else if statusCode == http.StatusConflict { logs.Info("Database conflict: " + msg + ". Code: " + strconv.Itoa(statusCode)) } else { logs.ErrorWithFields(map[string]interface{}{ "type": "Database error", "message": msg, "code": statusCode, }, err) } return ServerErrorStruct{ error: errors.Error(errorString), Message: msg, } } // implements error so that API handler can extract the msg type ServerErrorStruct struct { error Message string } func NewServerError(err error, msg string) error { return ServerErrorStruct{ error: err, Message: msg, } } func DatabaseServerError(err error, msg string) (events.APIGatewayProxyResponse, error) { statusCode := StatusCodeFromSQLError(err) errorString := err.Error() if dbError := ErrorFromDBError(err); dbError != "" { errorString = dbError if strings.HasSuffix(msg, ".") { // Remove trailing full stop before adding dbError. msg = strings.TrimSuffix(msg, ".") } msg = msg + ": " + dbError } if statusCode == http.StatusNotFound { logs.Info("Database error: " + msg + ". Code: " + strconv.Itoa(statusCode)) } else if statusCode == http.StatusConflict { logs.Info("Database conflict: " + msg + ". Code: " + strconv.Itoa(statusCode)) } else { logs.ErrorWithFields(map[string]interface{}{ "type": "Database error", "message": msg, "code": statusCode, }, err) } serverError := errorMsg{ Message: msg, Error: errorString, } bodyBytes, marshalError := json.Marshal(serverError) if marshalError != nil { return events.APIGatewayProxyResponse{ StatusCode: statusCode, Headers: map_utils.MergeMaps(utils.CorsHeaders(), responses.ContentTypeJSONHeader), Body: "{ \"error\": \"" + http.StatusText(http.StatusInternalServerError) + "\"}", }, nil } // Don't send an error on DB conflict if statusCode == http.StatusConflict { err = nil } return events.APIGatewayProxyResponse{ StatusCode: statusCode, Headers: map_utils.MergeMaps(utils.CorsHeaders(), responses.ContentTypeJSONHeader), Body: string(bodyBytes), }, err } func ErrorFromDBError(err error) string { pgErr, ok := err.(pg.Error) if !ok { return err.Error() } message := humanReadableDatabaseError(pgErr) return message } func humanReadableDatabaseError(pgErr pg.Error) string { postgresErrorCode := pgErr.Field('C') if postgresErrorCode == "23505" { // Conflict detail := pgErr.Field('D') if detail == "" { return pgErr.Error() } r, err := regexp.Compile("\\(.*?.*?\\)") // Match all between ( and ) if err != nil { return pgErr.Error() } matches := r.FindAllString(detail, -1) if len(matches) != 2 { return pgErr.Error() } keysString := matches[0] keysString = trimBrackets(keysString) keys := strings.Split(keysString, ",") conflictString := "" for _, key := range keys { cleanKey := strings.TrimSpace(key) if cleanKey == "provider_id" { continue // Don't check for provider ID uniqueness } cleanKey = strings.ReplaceAll(cleanKey, "_", " ") if conflictString == "" { conflictString = cleanKey } else { conflictString = conflictString + ", " + cleanKey } } message := fmt.Sprintf("The specified %s already exists", conflictString) return message } return pgErr.Error() } func trimBrackets(value string) string { value = strings.TrimPrefix(value, "(") value = strings.TrimSuffix(value, ")") return value } // ClientError creates responses due to request client error func ClientError(status int, message string) (events.APIGatewayProxyResponse, error) { logs.WarnWithFields(map[string]interface{}{ "type": "Client error", "code": status, }, errors.Error(message)) e := errorMsg{ Message: message, } b, err := json.Marshal(e) if err != nil { logs.Info("Could not create error messsage for ", message) } return events.APIGatewayProxyResponse{ StatusCode: status, Headers: map_utils.MergeMaps(utils.CorsHeaders(), responses.ContentTypeJSONHeader), Body: string(b), }, errors.Error(message) } func StatusCodeFromSQLError(err error) int { if err == pg.ErrNoRows { return http.StatusNotFound } pgErr, ok := err.(pg.Error) if !ok || pgErr == nil || !pgErr.IntegrityViolation() { return http.StatusInternalServerError } // See Postgres docs for error codes: https://www.postgresql.org/docs/10/errcodes-appendix.html postgresErrorCode := pgErr.Field('C') switch postgresErrorCode { case "23505": return http.StatusConflict default: return http.StatusInternalServerError } } func GenericJSONResponseWithContentAndHeaders(code int, content string, headers map[string]string) events.APIGatewayProxyResponse { response := events.APIGatewayProxyResponse{ StatusCode: code, Body: content, Headers: map_utils.MergeMaps(utils.CorsHeaders(), ContentTypeJSONHeader, headers), } return response } func GenericJSONResponseWithMessage(code int, err error) events.APIGatewayProxyResponse { var message string var body map[string]string if err != nil { customErr := err.(*errors.CustomError) message = customErr.Formatted(errors.FormattingOptions{NewLines: false, Causes: true}) body = map[string]string{ "message": string_utils.Capitalize(message), } } responseBody := message if bodyBytes, err := json.Marshal(body); err == nil { responseBody = string(bodyBytes) } return events.APIGatewayProxyResponse{ StatusCode: code, Body: responseBody, Headers: map_utils.MergeMaps(utils.CorsHeaders(), ContentTypeJSONHeader), } } func TimeResponse() events.APIGatewayProxyResponse { currentTime, _ := json.Marshal(date_utils.CurrentDate()) return events.APIGatewayProxyResponse{ StatusCode: http.StatusOK, Body: string(currentTime), Headers: map_utils.MergeMaps(utils.CorsHeaders(), ContentTypeJSONHeader), } }