You're reading a sample of this book. Get the full version here.
Let's Go Further Sending JSON Responses › Sending Error Messages
Previous · Contents · Next
Chapter 3.6.

Sending Error Messages

At this point our API is sending nicely formatted JSON responses for successful requests, but if a client makes a bad request — or something goes wrong in our application — we’re still sending them a plain-text error message from the http.Error() and http.NotFound() functions.

In this chapter we’ll fix that by creating some additional helpers to manage errors and send the appropriate JSON responses to our clients.

If you’re following along, go ahead and create a new cmd/api/errors.go file:

$ touch cmd/api/errors.go

And then add some helper methods like so:

File: cmd/api/errors.go
package main

import (
    "fmt"
    "net/http"
)

// The logError() method is a generic helper for logging an error message. Later in the
// book we'll upgrade this to use structured logging, and record additional information
// about the request including the HTTP method and URL.
func (app *application) logError(r *http.Request, err error) {
    app.logger.Println(err)
}

// 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 an interface{}
// 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 interface{}) {
    env := envelope{"error": message}

    // Write the response using the writeJSON() helper. If this happens to return an
    // error then log it, and fall back to sending the client an empty response with a
    // 500 Internal Server Error status code.
    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 your 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 method is not supported for this resource", r.Method)
    app.errorResponse(w, r, http.StatusMethodNotAllowed, message)
}

Now those are in place, let’s update our API handlers to use these new helpers instead of the http.Error() and http.NotFound() functions. Like so:

File: cmd/api/healthcheck.go
package main

...

func (app *application) healthcheckHandler(w http.ResponseWriter, r *http.Request) {
    env := envelope{
        "status": "available",
        "system_info": map[string]string{
            "environment": app.config.env,
            "version":     version,
        },
    }

    err := app.writeJSON(w, http.StatusOK, env, nil)
    if err != nil {
        // Use the new serverErrorResponse() helper.
        app.serverErrorResponse(w, r, err)
    }
}
File: cmd/api/movies.go
package main

...

func (app *application) showMovieHandler(w http.ResponseWriter, r *http.Request) {
    id, err := app.readIDParam(r)
    if err != nil {
        // Use the new notFoundResponse() helper.
        app.notFoundResponse(w, r)
        return
    }

    movie := data.Movie{
        ID:        id,
        CreatedAt: time.Now(),
        Title:     "Casablanca",
        Runtime:   102,
        Genres:    []string{"drama", "romance", "war"},
        Version:   1,
    }

    err = app.writeJSON(w, http.StatusOK, envelope{"movie": movie}, nil)
    if err != nil {
        // Use the new serverErrorResponse() helper.
        app.serverErrorResponse(w, r, err)
    }
}

Routing errors

Any error messages that our own API handlers send will now be well-formed JSON responses. Which is great!

But what about the error messages that httprouter automatically sends when it can’t find a matching route? By default, these will still be the same plain-text (non-JSON) responses that we saw earlier in the book.

Fortunately, httprouter allows us to set our own custom error handlers when we initialize the router. These custom handlers must satisfy the http.Handler interface, which is good news for us because it means we can easily re-use the notFoundResponse() and methodNotAllowedResponse() helpers that we just made.

Open up the cmd/api/routes.go file and configure the httprouter instance like so:

File: cmd/api/routes.go
package main

...

func (app *application) routes() *httprouter.Router {
    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.showMovieHandler)

    return router
}

Let’s test out these changes. Restart the application, then try making some requests for endpoints that don’t exist, or which use an unsupported HTTP method. You should now get some nice JSON error responses which look similar to these:

$ curl -i localhost:4000/foo
HTTP/1.1 404 Not Found
Content-Type: application/json
Date: Tue, 06 Apr 2021 15:13:42 GMT
Content-Length: 58

{
    "error": "the requested resource could not be found"
}

$ curl -i localhost:4000/v1/movies/abc
HTTP/1.1 404 Not Found
Content-Type: application/json
Date: Tue, 06 Apr 2021 15:14:01 GMT
Content-Length: 58

{
    "error": "the requested resource could not be found"
}

$ curl -i -X PUT localhost:4000/v1/healthcheck
HTTP/1.1 405 Method Not Allowed
Allow: GET, OPTIONS
Content-Type: application/json
Date: Tue, 06 Apr 2021 15:14:21 GMT
Content-Length: 66

{
    "error": "the PUT method is not supported for this resource"
}

In this final example, notice that httprouter still automatically sets the correct Allow header for us, even though it is now using our custom error handler for the response?


Additional Information

System-generated error responses

While we’re on the topic of errors, I’d like to mention that in certain scenarios Go’s http.Server may still automatically generate and send plain-text HTTP responses. These scenarios include when:

For example, if we try sending a request with an invalid Host header value we will get a response like this:

$ curl -i -H "Host: こんにちは"  http://localhost:4000/v1/healthcheck
HTTP/1.1 400 Bad Request: malformed Host header
Content-Type: text/plain; charset=utf-8
Connection: close

400 Bad Request: malformed Host header

Unfortunately, these responses are hard-coded into the Go standard library, and there’s nothing we can do to customize them to use JSON instead.

But while this is something to be aware of, it’s not necessarily something to worry about. In a production environment it’s relatively unlikely that well-behaved, non-malicious, clients would trigger these responses anyway, and we shouldn’t be overly concerned if bad clients are sometimes sent a plain-text response instead of JSON.