Skip to content

Commit

Permalink
Basic setup of v1 common utils (#282)
Browse files Browse the repository at this point in the history
  • Loading branch information
aidantrabs authored Dec 23, 2024
2 parents 27b0551 + 44ff8c9 commit b79ca7b
Show file tree
Hide file tree
Showing 11 changed files with 877 additions and 291 deletions.
124 changes: 70 additions & 54 deletions backend/internal/server/error_handler.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package server

import (
"KonferCA/SPUR/internal/v1/v1_common"
"encoding/json"
"fmt"
"net/http"
"strings"

"github.com/go-playground/validator/v10"
"github.com/labstack/echo/v4"
Expand All @@ -26,80 +30,92 @@ Example:
})
*/
func errorHandler(err error, c echo.Context) {
req := c.Request()
internalErr, ok := c.Get("internal_error").(error)
if !ok {
internalErr = nil
}
requestID := req.Header.Get(echo.HeaderXRequestID)
var (
code = http.StatusInternalServerError
message = "internal server error"
errType = v1_common.ErrorTypeInternal
details string
)

// default error response
status := http.StatusInternalServerError
message := "internal server error"
var validationErrors []string
internalErr, _ := c.Get("internal_error").(error)

// handle different error types
switch e := err.(type) {
case *echo.HTTPError:
status = e.Code
// since the echo.HTTPError allows type any for the
// message field, we should make sure that it is an
// actual string that was passed before using it.
// problems can arise if an struct was passed but
// not meant to be exposed to the public or
// is just straight up unreadable.
code = e.Code
errType = v1_common.DetermineErrorType(code)
if msg, ok := e.Message.(string); ok {
message = msg
} else {
message = http.StatusText(e.Code)
message = http.StatusText(code)
}

case validator.ValidationErrors:
// handle validation errors specially
status = http.StatusBadRequest
code = http.StatusBadRequest
errType = v1_common.ErrorTypeValidation
message = "validation failed"
validationErrors = make([]string, len(e))
for i, err := range e {
validationErrors[i] = err.Error()
fieldErrors := make(map[string]map[string]interface{})

for _, err := range e {
fieldName := strings.ToLower(err.Field())
fieldErrors[fieldName] = map[string]interface{}{
"tag": err.Tag(),
"value": err.Value(),
"condition": err.Param(),
}
}

case error:
// assign the returned error from handlers as the internal error.
// this is probably an internal error when trying to respond.
// this ensures that no internal error message gets leaks to the public.
if internalErr == nil {
internalErr = err
detailsBytes, err := json.Marshal(fieldErrors)
if err != nil {
details = "error formatting validation details"
log.Error().Err(err).Msg("failed to format validation details")
} else {
details = string(detailsBytes)
}
case *v1_common.APIError:
code = e.Code
errType = e.Type
message = e.Message
details = e.Details
default:
if internalErr != nil {
log.Error().Err(internalErr).Msg("internal error occurred")
} else {
log.Error().Err(err).Msg("unexpected error occurred")
}
details = "an unexpected error occurred. please try again later"
}

requestID := c.Request().Header.Get(echo.HeaderXRequestID)

// log with more context
log.
Error().
AnErr("internal_error", internalErr).
AnErr("request_error", err).
logContext := log.With().
Str("request_error", fmt.Sprintf("code=%d, message=%s", code, message)).
Str("request_id", requestID).
Str("method", req.Method).
Str("path", req.URL.Path).
Int("status", status).
Str("user_agent", req.UserAgent()).
Msg("request error")
Str("method", c.Request().Method).
Str("path", c.Request().URL.Path).
Int("status", code).
Str("user_agent", c.Request().UserAgent())

// return json response
if !c.Response().Committed {
response := ErrorResponse{
Status: status,
Message: message,
RequestID: requestID,
}
if len(validationErrors) > 0 {
response.Errors = validationErrors
}
if internalErr != nil {
logContext = logContext.Str("error", internalErr.Error())
} else if err != nil && code == http.StatusInternalServerError {
logContext = logContext.Str("error", err.Error())
}

logger := logContext.Logger()
logger.Error().Msg("request error")

if err := c.JSON(status, response); err != nil {
log.Error().
Err(err).
Str("request_id", requestID).
Msg("failed to send error response")
apiError := &v1_common.APIError{
Type: errType,
Message: message,
Details: details,
RequestID: requestID,
Code: code,
}

if !c.Response().Committed {
if err := c.JSON(code, apiError); err != nil {
log.Error().Err(err).Msg("failed to send error response")
}
}
}
Loading

0 comments on commit b79ca7b

Please sign in to comment.