Multipart HTTP responses in Go

2019 02 12

Sometimes I write HTTP servers that need to serve multiple values in response to a single request. If the values are small, one common way is to define an e.g. JSON object to wrap them.

type myResponse struct {
	Values []string `json:"values"`
}

func handle(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	json.NewEncoder(w).Encode(myResponse{
		Values: getValues(),
	})
}

But sometimes that’s not a great solution; for example, if the values are raw binary data (in Go, []byte) and you don’t want to go through a base64 conversion. In those cases, it may make sense to use a multipart response. For the record, this approach is adapted (reverse engineered, I guess) from the Riak KV API.

Here’s one way to set things up in the handler.

func handle(w http.ResponseWriter, r *http.Request) {
	mediatype, _, err := mime.ParseMediaType(r.Header.Get("Accept"))
	if err != nil {
		http.Error(w, err.Error(), http.StatusNotAcceptable)
		return
	}
	if mediatype != "multipart/form-data" {
		http.Error(w, "set Accept: multipart/form-data", http.StatusMultipleChoices)
		return
	}
	mw := multipart.NewWriter(w)
	w.Header().Set("Content-Type", mw.FormDataContentType())
	for _, value := range getValues() {
		fw, err := mw.CreateFormField("value")
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		if _, err := fw.Write(value); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
	}
	if err := mw.Close(); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

And here’s how to use it as a consumer, with error handling elided.

func main() {
	req, _ := http.NewRequest("GET", "http://localhost:8080/foo", nil)
	req.Header.Set("Accept", "multipart/form-data; charset=utf-8")
	resp, _ := http.DefaultClient.Do(req)
	_, params, _ := mime.ParseMediaType(resp.Header.Get("Content-Type"))
	mr := multipart.NewReader(resp.Body, params["boundary"])
	for part, err := mr.NextPart(); err == nil; part, err = mr.NextPart() {
		value, _ := ioutil.ReadAll(part)
		log.Printf("Value: %s", value)
	}
}

Hopefully that helps someone. Is there a better way to do it? Tweet at me and I’ll update the code.

Related work: if you’re interested in streaming potentially unlimited data from an HTTP server to a client, and don’t want to deal with Websockets (I don’t blame you) consider using eventsourcing, also known as server-sent events.