diff --git a/cmd/api/errors.go b/cmd/api/errors.go index b42f1b0..f597b8c 100644 --- a/cmd/api/errors.go +++ b/cmd/api/errors.go @@ -35,3 +35,7 @@ func (app *application) methodNotAllowedResponse(w http.ResponseWriter, r *http. message := fmt.Sprintf("the %s method is not supported for this resource", r.Method) app.errorResponse(w, r, http.StatusMethodNotAllowed, message) } + +func (app *application) badRequestResponse(w http.ResponseWriter, r *http.Request, err error) { + app.errorResponse(w, r, http.StatusBadRequest, err.Error()) +} diff --git a/cmd/api/healthcheck.go b/cmd/api/healthcheck.go index c107b72..1fd531b 100644 --- a/cmd/api/healthcheck.go +++ b/cmd/api/healthcheck.go @@ -16,8 +16,7 @@ func (app *application) healthCheckHandler(w http.ResponseWriter, r *http.Reques err := app.writeJSON(w, http.StatusOK, envelopedHealth, nil) if err != nil { - app.logger.Print(err) - http.Error(w, "the server encountered an error and could process your request", http.StatusInternalServerError) + app.serverErrorResponse(w, r, err) } } diff --git a/cmd/api/helpers.go b/cmd/api/helpers.go index 8f47c62..087ec39 100644 --- a/cmd/api/helpers.go +++ b/cmd/api/helpers.go @@ -3,14 +3,70 @@ package main import ( "encoding/json" "errors" + "fmt" + "io" "net/http" "strconv" + "strings" "github.com/julienschmidt/httprouter" ) type envelope map[string]any +func (app *application) readJSON(w http.ResponseWriter, r *http.Request, dest any) error { + maxBytes := 1_048_576 + r.Body = http.MaxBytesReader(w, r.Body, int64(maxBytes)) + + decoder := json.NewDecoder(r.Body) + decoder.DisallowUnknownFields() + + err := decoder.Decode(dest) + + if err != nil { + var syntaxError *json.SyntaxError + var unmarshallError *json.UnmarshalTypeError + var invalidUnmarshall *json.InvalidUnmarshalError + var maxBytesError *http.MaxBytesError + + switch { + case errors.As(err, &syntaxError): + return fmt.Errorf("body contains badly-formed JSON (at character %d)", syntaxError.Offset) + + case errors.Is(err, io.ErrUnexpectedEOF): + return errors.New("body contains badly-formed JSON") + + case errors.As(err, &unmarshallError): + if unmarshallError.Field != "" { + return fmt.Errorf("the body contains incorrect json type for field %q", unmarshallError.Field) + } + return fmt.Errorf("body contains incorrect JSON type (at character %d)", unmarshallError.Offset) + + case errors.Is(err, io.EOF): + return errors.New("body must not be empty") + + case strings.HasPrefix(err.Error(), "json: unknown field "): + fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ") + return fmt.Errorf("body contains uknown key %s", fieldName) + + case errors.As(err, &maxBytesError): + return fmt.Errorf("body must not be larger than %d bytes", maxBytesError.Limit) + + case errors.As(err, &invalidUnmarshall): + panic(err) + + default: + return err + } + } + err = decoder.Decode(&struct{}{}) + if err != io.EOF { + return errors.New("body must only contain a single JSON value") + } + + return nil // no errors +} + func (app *application) readIDParam(r *http.Request) (int64, error) { params := httprouter.ParamsFromContext(r.Context()) diff --git a/cmd/api/movies.go b/cmd/api/movies.go index a259a39..433a783 100644 --- a/cmd/api/movies.go +++ b/cmd/api/movies.go @@ -9,13 +9,26 @@ import ( ) func (app *application) createMovieHandler(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "create a new movie") + var input struct { + Title string `json:"title"` + Year int32 `json:"year"` + Runtime int32 `json:"runtime"` + Genres []string `json:"genres"` + } + + err := app.readJSON(w, r, &input) + if err != nil { + app.badRequestResponse(w, r, err) + return + } + + fmt.Fprintf(w, "%v+\n", input) } func (app *application) showMovieHandler(w http.ResponseWriter, r *http.Request) { id, err := app.readIDParam(r) if err != nil { - http.NotFound(w, r) + app.notFoundResponse(w, r) return } @@ -31,8 +44,7 @@ func (app *application) showMovieHandler(w http.ResponseWriter, r *http.Request) envelopedMovie := envelope{"movie": movie} err = app.writeJSON(w, http.StatusOK, envelopedMovie, nil) if err != nil { - app.logger.Print(err) - http.Error(w, "the server encountered an error and could process your request", http.StatusInternalServerError) + app.serverErrorResponse(w, r, err) } } diff --git a/cmd/api/routes.go b/cmd/api/routes.go index 4032ad7..a20ee47 100644 --- a/cmd/api/routes.go +++ b/cmd/api/routes.go @@ -8,6 +8,8 @@ import ( func (app *application) routes() *httprouter.Router { router := httprouter.New() + router.NotFound = http.HandlerFunc(app.notFoundResponse) + router.MethodNotAllowed = http.HandlerFunc(app.methodNotAllowedResponse) router.HandlerFunc(http.MethodGet, "/v1/healthcheck", app.healthCheckHandler) router.HandlerFunc(http.MethodPost, "/v1/movies", app.createMovieHandler)