From c726f59b6e1ea4b5bd9142ff6981c9ea43f24342 Mon Sep 17 00:00:00 2001 From: Drew Bednar Date: Sat, 12 Apr 2025 11:50:38 -0400 Subject: [PATCH] Adding middleware and generic error handling --- cmd/api/errors.go | 56 +++++++++++++++++++++++++++++++++++++++++++ cmd/api/handlers.go | 14 +++++++---- cmd/api/middleware.go | 30 +++++++++++++++++++++++ cmd/api/routes.go | 12 +++++++++- 4 files changed, 106 insertions(+), 6 deletions(-) create mode 100644 cmd/api/errors.go create mode 100644 cmd/api/middleware.go diff --git a/cmd/api/errors.go b/cmd/api/errors.go new file mode 100644 index 0000000..a699040 --- /dev/null +++ b/cmd/api/errors.go @@ -0,0 +1,56 @@ +package main + +import ( + "fmt" + "net/http" +) + +// The logError() method is a generic helper for logging an error message along +// with the current request method and URL as attributes in the log entry. +func (app *application) logError(r *http.Request, err error) { + var ( + method = r.Method + uri = r.URL.RequestURI() + ) + app.logger.Error(err.Error(), "method", method, "uri", uri) +} + +// The errorResponse() method is a generic helper for sending JSON-formatted error +// messages to the client with a given status code. Note that we're using the any +// type for the message parameter, rather than just a string type, as this gives us +// more flexibility over the values that we can include in the response. +func (app *application) errorResponse(w http.ResponseWriter, r *http.Request, status int, message any) { + env := envelope{"error": message} + + err := app.writeJSON(w, status, env, nil) + if err != nil { + app.logError(r, err) + w.WriteHeader(500) + } + +} + +// The serverErrorResponse() method will be used when our application encounters an +// unexpected problem at runtime. It logs the detailed error message, then uses the +// errorResponse() helper to send a 500 Internal Server Error status code and JSON +// response (containing a generic error message) to the client. +func (app *application) serverErrorResponse(w http.ResponseWriter, r *http.Request, err error) { + app.logError(r, err) + + message := "the server encountered a problem and could not process the request" + app.errorResponse(w, r, http.StatusInternalServerError, message) +} + +// The notFoundResponse() method will be used to send a 404 Not Found status code and +// JSON response to the client. +func (app *application) notFoundResponse(w http.ResponseWriter, r *http.Request) { + message := "the requested resource could not be found" + app.errorResponse(w, r, http.StatusNotFound, message) +} + +// The methodNotAllowedResponse() method will be used to send a 405 Method Not Allowed +// status code and JSON response to the client. +func (app *application) methodNotAllowedResponse(w http.ResponseWriter, r *http.Request) { + message := fmt.Sprintf("the %s mehtod is not supported for this resource", r.Method) + app.errorResponse(w, r, http.StatusMethodNotAllowed, message) +} diff --git a/cmd/api/handlers.go b/cmd/api/handlers.go index 62a1970..bf54d18 100644 --- a/cmd/api/handlers.go +++ b/cmd/api/handlers.go @@ -26,7 +26,8 @@ func (app *application) getMovieHandler(w http.ResponseWriter, r *http.Request) id, err := strconv.ParseInt(params.ByName("id"), 10, 64) if err != nil || id < 1 { - http.NotFound(w, r) + // http.NotFound(w, r) + app.notFoundResponse(w, r) return } @@ -38,11 +39,13 @@ func (app *application) getMovieHandler(w http.ResponseWriter, r *http.Request) Genres: []string{"sci-fi", "action"}, Version: 1, } + app.logger.Info("Hit the get movies and found", "movie", movie.Title) err = app.writeJSON(w, http.StatusOK, envelope{"movie": movie}, nil) if err != nil { - app.logger.Error(err.Error()) - http.Error(w, "The server encountered a problem and could not process your request", http.StatusInternalServerError) + //app.logger.Error(err.Error()) + //http.Error(w, "The server encountered a problem and could not process your request", http.StatusInternalServerError) + app.serverErrorResponse(w, r, err) } } @@ -72,8 +75,9 @@ func (app *application) healthCheckHandler(w http.ResponseWriter, r *http.Reques // w.Write(js) err := app.writeJSON(w, 200, env, nil) if err != nil { - app.logger.Error(err.Error()) - http.Error(w, "The server encountered a problem and could not process your request", http.StatusInternalServerError) + //app.logger.Error(err.Error()) + //http.Error(w, "The server encountered a problem and could not process your request", http.StatusInternalServerError) + app.serverErrorResponse(w, r, err) return } } diff --git a/cmd/api/middleware.go b/cmd/api/middleware.go new file mode 100644 index 0000000..b3475a5 --- /dev/null +++ b/cmd/api/middleware.go @@ -0,0 +1,30 @@ +package main + +import ( + "fmt" + "net/http" +) + +// recoverPanic provides middleware for recovering from panics in the same goroutine that +// executred the recoverPanic middleware. +// +// This means that panics from goroutines created in handler operations will still need +// to be handled separately.Failure to do so will cause the application to exit and bring +// down the server. +func (app *application) recoverPanic(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Create a deferred function (which will always be run in the event of a panic + // as Go unwinds the stack). + defer func() { + if err := recover(); err != nil { + // If there was a panic, set a "Connection: close" header on the + // response. This acts as a trigger to make Go's HTTP server + // automatically close the current connection after a response has been + // sent. + w.Header().Set("Connection", "close") + app.serverErrorResponse(w, r, fmt.Errorf("%s", http.ErrAbortHandler)) + } + }() + next.ServeHTTP(w, r) + }) +} diff --git a/cmd/api/routes.go b/cmd/api/routes.go index 735dd15..6d32d8b 100644 --- a/cmd/api/routes.go +++ b/cmd/api/routes.go @@ -13,9 +13,19 @@ func (app *application) routes() http.Handler { // http.Handler rather than handler functions as seen below router := httprouter.New() + // Convert the notFoundResponse() helper to a http.Handler using the + // http.HandlerFunc() adapter, and then set it as the custom error handler for 404 + // Not Found responses. + router.NotFound = http.HandlerFunc(app.notFoundResponse) + + // Likewise, convert the methodNotAllowedResponse() helper to a http.Handler and set + // it as the custom error handler for 405 Method Not Allowed responses. + router.MethodNotAllowed = http.HandlerFunc(app.methodNotAllowedResponse) + router.HandlerFunc(http.MethodGet, "/v1/healthcheck", app.healthCheckHandler) router.HandlerFunc(http.MethodPost, "/v1/movies", app.createMovieHandler) router.HandlerFunc(http.MethodGet, "/v1/movies/:id", app.getMovieHandler) - return router + // middleware + return app.recoverPanic(router) }