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:
And then add some helper methods like so:
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:
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:
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:
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.
Panic recovery
At the moment any panics in our API handlers will be recovered automatically by Go’s http.Server. This will unwind the stack for the affected goroutine (calling any deferred functions along the way), close the underlying HTTP connection, and log an error message and stack trace.
This behavior is OK, but it would be better for the client if we could also send a 500 Internal Server Error response to explain that something has gone wrong — rather than just closing the HTTP connection with no context.
In Let’s Go we talked through the details of how to do this by creating some middleware to recover the panic, and it makes sense to do the same thing again here.
If you’re following along, go ahead and create a cmd/api/middleware.go file:
And inside that file add a new recoverPanic() middleware:
Once that’s done, we need to update our cmd/api/routes.go file so that the recoverPanic() middleware wraps our router. This will ensure that the middleware runs for every one of our API endpoints.
Now that’s in place, if there is a panic in one of our API handlers the recoverPanic() middleware will recover it and call our regular app.serverErrorResponse() helper. In turn, that will log the error using our structured logger and send the client a nice 500 Internal Server Error response with a JSON body.
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:
The HTTP request specifies an unsupported HTTP protocol version.
The HTTP request contains a missing or invalid Host header, or multiple Host headers.
The HTTP request contains a empty Content-Length header.
The HTTP request contains an unsupported Transfer-Encoding header.
The size of the HTTP request headers exceeds the server’s MaxHeaderBytes setting.
The client makes a HTTP request to a HTTPS server.
For example, if we try sending a request with an invalid Host header value we will get a response like this:
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.
Panic recovery in other goroutines
It’s really important to realize that our middleware will only recover panics that happen in the same goroutine that executed the recoverPanic() middleware.
If, for example, you have a handler which spins up another goroutine (e.g. to do some background processing), then any panics that happen in the background goroutine will not be recovered — not by the recoverPanic() middleware… and not by the panic recovery built into http.Server. These panics will cause your application to exit and bring down the server.
So, if you are spinning up additional goroutines from within your handlers and there is any chance of a panic, you must make sure that you recover any panics from within those goroutines too.
We’ll look at this topic in more detail later in the book, and demonstrate how to deal with it when we use a background goroutine to send welcome emails to our API users.