Select Git revision
api_responses.go
-
Francé Wilke authoredFrancé Wilke authored
api_responses.go 8.06 KiB
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),
}
}