Quellcode durchsuchen

generic error handling

Alexander Belanger vor 5 Jahren
Ursprung
Commit
885f9607d0
4 geänderte Dateien mit 101 neuen und 23 gelöschten Zeilen
  1. 3 1
      internal/validator/validator.go
  2. 93 18
      server/api/errors.go
  3. 4 4
      server/api/user_handler.go
  4. 1 0
      server/router/router.go

+ 3 - 1
internal/validator/validator.go

@@ -1,9 +1,11 @@
 package validator
 
 import (
-	"gopkg.in/go-playground/validator.v9"
+	"github.com/go-playground/validator/v10"
 )
 
+// New creates a new instance of validator and sets the tag name
+// to "form", instead of "validate"
 func New() *validator.Validate {
 	validate := validator.New()
 	validate.SetTagName("form")

+ 93 - 18
server/api/errors.go

@@ -5,40 +5,115 @@ import (
 	"fmt"
 	"net/http"
 
-	"gopkg.in/go-playground/validator.v9"
+	"github.com/go-playground/validator/v10"
 )
 
 const (
-	appErrDataCreationFailure = "data creation failure"
-	appErrFormDecodingFailure = "form decoding failure"
+	appErrDataWrite    = "data write error"
+	appErrFormDecoding = "could not process JSON body"
 )
 
-func (app *App) handleUnprocessableEntity(err error, w http.ResponseWriter) {
-	app.logger.Warn().Err(err).Msg("")
-	w.WriteHeader(http.StatusUnprocessableEntity)
-	fmt.Fprintf(w, `{"error": "%v"}`, appErrFormDecodingFailure)
+// HTTPError is the object returned when the API encounters an error: this
+// gets marshaled into JSON
+type HTTPError struct {
+	Code   ErrorCode `json:"code"`
+	Errors []string  `json:"errors"`
 }
 
-func (app *App) handleErrorFormValidation(err error, w http.ResponseWriter) {
+// ErrorCode is a custom Porter error code, useful for frontend messages
+type ErrorCode int64
+
+// Enumeration of API error codes, represented as int64
+const (
+	ErrUserDecode ErrorCode = iota
+	ErrUserValidateFields
+	ErrUserDataWrite
+)
+
+// ------------------------ Error helper functions ------------------------ //
+
+// sendExternalError marshals an HTTPError into JSON: this function will return an error if
+// a marshaling error occurs, but only after the internal error header has been sent to the
+// client.
+//
+// It then logs it via the app.logger and sends a formatted error to the client.
+func (app *App) sendExternalError(err error, errExt HTTPError, w http.ResponseWriter) (intErr error) {
+	respBytes, newErr := json.Marshal(errExt)
+
+	if newErr != nil {
+		app.handleErrorInternalError(newErr, w)
+		return newErr
+	}
+
+	respBody := string(respBytes)
+
+	app.logger.Warn().Err(err).
+		Str("errExt", respBody).
+		Msg("")
+
+	fmt.Fprintf(w, respBody)
+
+	return nil
+}
+
+// handleErrorFormDecoding handles an error in decoding process from JSON to the
+// construction of a Form model, and the conversion between a form model and a
+// gorm.Model.
+func (app *App) handleErrorFormDecoding(err error, code ErrorCode, w http.ResponseWriter) {
+	errExt := HTTPError{
+		Code:   code,
+		Errors: []string{appErrFormDecoding},
+	}
+
+	intErr := app.sendExternalError(err, errExt, w)
+
+	if intErr == nil {
+		w.WriteHeader(http.StatusUnprocessableEntity)
+	}
+}
+
+// handleErrorFormValidation handles an error in the validation of form fields, and
+// sends a descriptive method about the incorrect fields to the client.
+func (app *App) handleErrorFormValidation(err error, code ErrorCode, w http.ResponseWriter) {
 	// translate all validator errors
 	errs := err.(validator.ValidationErrors)
-	translation := errs.Translate(*app.translator)
-	respBody, newErr := json.Marshal(translation)
+	res := make([]string, 0)
 
-	if newErr != nil {
-		app.handleGenericInternalError(newErr, w)
+	for _, field := range errs {
+		valErr := field.Tag() + " validation failed"
+
+		res = append(res, valErr)
+	}
+
+	errExt := HTTPError{
+		Code:   code,
+		Errors: res,
 	}
 
-	fmt.Fprintf(w, `{"errors": %v}`, respBody)
+	intErr := app.sendExternalError(err, errExt, w)
+
+	if intErr == nil {
+		w.WriteHeader(http.StatusUnprocessableEntity)
+	}
 }
 
-func (app *App) handleDataWriteFailure(err error, w http.ResponseWriter) {
-	app.logger.Warn().Err(err).Msg("")
-	w.WriteHeader(http.StatusInternalServerError)
-	fmt.Fprintf(w, `{"error": "%v"}`, appErrDataCreationFailure)
+// handleErrorDataWrite handles a database write error
+func (app *App) handleErrorDataWrite(err error, code ErrorCode, w http.ResponseWriter) {
+	errExt := HTTPError{
+		Code:   code,
+		Errors: []string{appErrDataWrite},
+	}
+
+	intErr := app.sendExternalError(err, errExt, w)
+
+	if intErr == nil {
+		w.WriteHeader(http.StatusUnprocessableEntity)
+	}
 }
 
-func (app *App) handleGenericInternalError(err error, w http.ResponseWriter) {
+// handleErrorInternalError is a catch-all for internal errors that occur during the
+// processing of a request
+func (app *App) handleErrorInternalError(err error, w http.ResponseWriter) {
 	app.logger.Warn().Err(err).Msg("")
 	w.WriteHeader(http.StatusInternalServerError)
 	fmt.Fprintf(w, `{"error": "Internal server error"}`)

+ 4 - 4
server/api/user_handler.go

@@ -15,26 +15,26 @@ func (app *App) HandleCreateUser(w http.ResponseWriter, r *http.Request) {
 	form := &models.CreateUserForm{}
 
 	if err := json.NewDecoder(r.Body).Decode(form); err != nil {
-		app.handleUnprocessableEntity(err, w)
+		app.handleErrorFormDecoding(err, ErrUserDecode, w)
 		return
 	}
 
 	if err := app.validator.Struct(form); err != nil {
-		app.handleUnprocessableEntity(err, w)
+		app.handleErrorFormValidation(err, ErrUserValidateFields, w)
 		return
 	}
 
 	userModel, err := form.ToUser()
 
 	if err != nil {
-		app.handleUnprocessableEntity(err, w)
+		app.handleErrorFormDecoding(err, ErrUserDecode, w)
 		return
 	}
 
 	user, err := queries.CreateUser(app.db, userModel)
 
 	if err != nil {
-		app.handleDataWriteFailure(err, w)
+		app.handleErrorDataWrite(err, ErrUserDataWrite, w)
 		return
 	}
 

+ 1 - 0
server/router/router.go

@@ -16,6 +16,7 @@ func New(a *api.App) *chi.Mux {
 		r.Use(middleware.ContentTypeJSON)
 
 		// /api/users routes
+		r.Method("POST", "/users", requestlog.NewHandler(a.HandleCreateUser, l))
 		r.Method("GET", "/users", requestlog.NewHandler(a.HandleReadUser, l))
 	})