Not sure how to structure your Go web application?
My new book guides you through the start-to-finish build of a real world web application in Go — covering topics like how to structure your code, manage dependencies, create dynamic database-driven pages, and how to authenticate and authorize users securely.
Let's say that you're building a JSON API with Go. And in some of the handlers — probably as part of a POST or PUT request — you want to read a JSON object from the request body and assign it to a struct in your code.
After a bit of research, there's a good chance that you'll end up with some code that looks similar to the personCreate handler here:
If you're putting together a quick prototype, or building an API for personal/internal use only, then the code in the personCreate handler is probably OK.
But if you're building an API for public use in production then there are a few issues with this to be aware of, and things that can be improved.
Not all errors returned by Decode() are caused by a bad request from the client. Specifically, Decode() can return a json.InvalidUnmarshalError error — which is caused by an unmarshalable target destination being passed to Decode(). If that happens, then it indicates a problem with your application — not the client request — so really the error should be logged and a 500 Internal Server Error response sent to the client instead.
The error messages returned by Decode() aren't ideal for sending to a client. Some are arguably too detailed and expose information about the underlying program (like "json: cannot unmarshal number into Go struct field Person.Name of type string"). Others aren't descriptive enough (like "unexpected EOF") and some are just plain confusing (like "invalid character 'A' looking for beginning of object key string"). There also isn't consistency in the formatting or language used.
A client can include extra unexpected fields in their JSON, and these fields will be silently ignored without the client receiving any error. We can fix this by using the decoder's DisallowUnknownFields() method.
There's no upper limit on the size of the request body that will be read by the Decode() method. Limiting this would help prevent our server resources being wasted if a malcious client sends a very large request body, and it's something we can easily do by using the http.MaxBytesReader() function.
There's no check for a Content-Type: application/json header in the request. Of course, this header may not always be present, and mistakes and malicious clients mean that it isn't a guarantee of the actual content type. But checking for an incorrect Content-Type header would allow us to 'fail fast' if there is an unexpected content-type provided, and we can send the client a helpful error message without spending unnecessary resources on parsing the request body.
The decoder that we create with json.NewDecoder() is designed to decode streams of JSON objects and considers a request body like '{"Name": "Bob"}{"Name": "Carol": "Age": 54}' or '{"Name": "Dave"}{}' to be valid. But in the code above only the first JSON object in the request body will actually be parsed. So if the client sends multiple JSON objects in the request body, we want to alert them to the fact that only a single object is supported.
There are two ways to achieve this. We can either call the decoder's Decode() method for a second time and make sure that it returns an io.EOF error (if it does, then we know there are not any additional JSON objects or other data in the request body). Or we could avoid using Decode() altogether and read the body into a byte slice and pass it to json.Unmarshal(), which would return an error if the body contains multiple JSON objects. The downside of using json.Unmarshal() is that there is no way to disallow extra unexpected fields in the JSON, so we can't address point 3 above.
An Improved Handler
Let's implement an alternative version of the personCreate handler which addresses all of these issues.
You'll notice here that we're using the new errors.Is() and errors.As() functions, which have been introduced in Go 1.13, to help intercept the errors from Decode().
The clear downside here is that this code is a lot more verbose, and IMO, a little bit ugly. Things aren't helped by the fact that there are quite a few open issues with json/encoding which are on hold pending a wider review of the package.
But from a security and client perspective it's a lot better 😊
The handler is now stricter about the content it will accept; we're reducing the amount of server resources used unnecessarily; and the client gets clear and consistent error messages that provide a decent amount of information without over-sharing.
As a side note, you might have noticed that the json/encoding package contains some other error types (like json.UnmarshalFieldError) which aren't checked in the code above — but these have been deprecated and not used by the current version of Go.
Making a Helper Function
If you've got a few handlers that need to to process JSON request bodies, you probably don't want to repeat this code in all of them.
A solution which I've found works well is to create a decodeJSONBody() helper function, and have this return a custom malformedRequest error type which wraps the errors and relevant status codes.
For example:
Once that's written, the code in your handlers can be kept really nice and compact:
If you enjoyed this article, you might like to check out my recommended tutorials list or check out my books Let's Go and Let's Go Further, which teach you everything you need to know about how to build professional production-ready web applications and APIs with Go.