5

Multipart HTTP responses in Go

 3 years ago
source link: https://peter.bourgon.org/blog/2019/02/12/multipart-http-responses.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK