Journal

Writing down the things I learned. To share them with others and my future self.

07 Jun 2020

The Non-Obvious properties of JSON Encoder.Encode directly to http.ResponseWriter

Today I came across the following code snipped in a golang HTTP handler:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import(
	"encoding/json"
	"net/http"
	"fmt"
)

func handler(w http.ResponseWriter, r *http.Request) {
	// [... handler code...]
	if err := json.NewEncoder(w).Encode(myApplicationResponse); err != nil {
		fmt.Printf("could not marshal the response: %v\n", err)
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
}

My first reaction was that this does not work as indented. The documentation of the ResponseWriter states the following:

If WriteHeader is not called explicitly, the first call to Write will trigger an implicit WriteHeader(http.StatusOK). Thus explicit calls to WriteHeader are mainly used to send error codes.

I thought that the Encoder will write to the given io.Writer while encoding the JSON. So this combined with the w.WriteHeader(http.StatusInternalServerError) in case of an error will cause a superfluous WriteHeader call, since the encoding called already w.Write() and thus setting the response code to 200. The superfluous WriterHeader call will be ignored. This is logicall, since the body is send to the client after the HTTP header. The status code is part of the header. Hence the status code must be written before the first body byte. The ReponseWriter streams the body to the client while the application HTTP handler is still running.

After digging into the Encoder code I learned that the JSON is encoded into a temporary buffer and after that copied to the given io.Writer.

I have three tackaways of this. First of all, the given code works as intented. It sets the status code 503 if the encoding fails. Secondly, the current behaviour (encode complete, than write) is not documented. It might change in the future. Hence the above snipped may not work as intended in the future. This post is written as of Golang 1.14.

The third takeaway is that Encoder.Encode can allocate alot of memory. For example, your JSON struct has a encoded size of 1MB and your application serves 1000 Requests/s. The memory consumption of the temporary encoding buffers alone is roughly 1 GB.