Skip to content
Snippets Groups Projects
Select Git revision
  • main default protected
  • trading_hours
  • refactor_trading_hours
  • audit_cleaning_cater_for_non_struct_fields
  • remove-info-logs
  • sl-refactor
  • 18-use-scan-for-param-values
  • 17-order-search-results
  • 4-simplify-framework-2
  • 1-http-error
  • v1.296.0
  • v1.295.0
  • v1.294.0
  • v1.293.0
  • v1.292.0
  • v1.291.0
  • v1.290.0
  • v1.289.0
  • v1.288.0
  • v1.287.0
  • v1.286.0
  • v1.285.0
  • v1.284.0
  • v1.283.0
  • v1.282.0
  • v1.281.0
  • v1.280.0
  • v1.279.0
  • v1.278.0
  • v1.277.0
30 results

api_responses.go

Blame
  • 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),
    	}
    }