HTTP JSON Error Responses in Go
I like simple structured messages using JSON in error responses, similar to Stripe, Uber and many others…
{
"error": {
"type": "validation_failed",
"message": "Username is a required field"
}
}
In my last story, I wrote about HTTP Logging and in that I mentioned that I have used “chained” middleware using the Adapter pattern from Mat Ryer’s excellent post. You’ll see that below, but also in addition, I’m wrapping my final true app handler (in this case, CreateUser) inside an ErrHandler type — eh.ErrHandler{Env: env, H: handler.CreateUser} (you’ll note I’m passing in a global environment type as well). I’m doing this based on another great article by Matt Silverlock on his blog here.
I’m using Matt’s article almost word for word for error handling, but made a few tweaks so that I could return a structured JSON response. The package is below:
I renamed the StatusError struct to HTTPErr and added Type (string) as a field to it. I then added an ErrType() method to the Error interface and to the HTTPErr struct in order to be able to extract the Type field. I added a couple of structs to help form the JSON (errResponse and svcError) in the manner I wanted and then populate and marshal those structs into JSON within the type switch logic. That’s it, really — Matt did all the heavy lifting, I just added some bits that I thought were helpful for my purposes.
This whole thing makes error handling pretty nice though — given the wrapper logic, you’ll always return a pretty good looking error. When creating errors within your app, you don’t have to have every error take this form — you can return normal errors lower down in the code and, depending on how you organize your code, you can catch and form the HTTPErr at a very high level so you’re not having to deal with populating a cumbersome struct all the way throughout your code. The below basically assumes that the usr.Create method is doing validations and as such we can add the “validation_failed” type to all errors returned from this method.
tx, err := usr.Create(ctx, env)
if err != nil {
return errorHandler.HTTPErr{
Code: http.StatusBadRequest,
Type: "validation_failed",
Err: err,
}
}
I also added a SetErr method to the HTTPErr struct, which allows you to initialize the struct with some default values and add the actual error on the fly. For instance, below as part of my super high level edit checks on my service inputs, I initialize the HTTPErr object at the beginning of my function and then perform my edit checks to allow for brevity in error creation.
func newUser(ctx context.Context, env *env.Env, cur *createUserRequest) (*appUser.User, error) {// declare a new instance of appUser.User
usr := new(appUser.User)// initialize an errorHandler with the default Code and Type for
// service validations (Err is set to nil as it will be set later)
e := errorHandler.HTTPErr{
Code: http.StatusBadRequest,
Type: "validation_error",
Err: nil,
}// for each field you can go through whatever validations you wish
// and use the SetErr method of the HTTPErr struct to add the proper
// error text
switch {
// Username is required
case cur.Username == "":
e.SetErr("Username is a required field")
return nil, e
// Username cannot be blah...
case cur.Username == "blah":
e.SetErr("Username cannot be blah")
return nil, e
// If we get through the switch, set the field
default:
usr.Username = cur.Username
}
.....
That’s it for now — I should note that Rob Pike and Andrew Gerrand authored a great post on error handling that is likely amazing. I need to ingest that and see how I can fold that into this as well…