Sending Images in POST request as MultiPart Form from Go to Microservice

Today we will learn how to send an image to a microservice for analysis and return the result to the user.

Sending Images in POST request as MultiPart Form from Go to Microservice
Photo by Caspar Camille Rubin / Unsplash

This problem occurred as I was trying to detect objects in an image with AI. I was able to train a server side object detection algorithm. A client specific solution i.e. with Create ML on iOS would be premature optimisation and makes more sense once the use case is well established. For now I want to be able to update the object detection algorithm anytime with improved training data.

We will talk about

  1. Skeleton and imports
  2. Manipulate binary data from an image
  3. Multipart Messages
  4. HTTP Post request
  5. HTTP Response
  6. Conclusion
  7. Full Listing

There are two ways to handle user images. The first is to upload the image to an image hoster like cloudinary or uploadcare and then pass just the URL to the server. I actually suggest to use this method and I have written a post about this earlier . But sometimes you may want to handle images at the server directly.

Simple text forms can be send as x-www-form-urlencoded. But we will create a multipart/form-data POST request in Go to forward images.

Skeleton and imports

Let’s start with a skeleton that contains imports and main function. The main function launches a server that listens on port 5000. It will call the function handleRequest once the url http://127.0.0.1:5000/image is called. This function will load an image file from disk in the same folder, send it to an analysis microservice and receives the result.

package main

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"mime/multipart"
	"net/http"
	"net/url"
	"os"
	"time"
)

func main() {
	handler := http.HandlerFunc(handleRequest)
	http.Handle("/image", handler)
	fmt.Println("Server starts at port 5000")
	http.ListenAndServe(":5000", nil)
}

func handleRequest(w http.ResponseWriter, r *http.Request) { 

// TODO: send image to microservice

}

Manipulate binary data from an image

In contrast to Python is Go a language that makes memory management and pointer visible. That makes it performant, but also requires some down to the metal thinking. The & operator returns the memory address of a variable. We create a http.Client. Since we don’t want to stall our server, just because the microservice is not available, we set a Timeout.

When dealing with text we use strings to perform operations like split or search. When dealing with data e.g. from images, this is done in a buffer of &bytes, where the & operator refers again to the memory address of that buffer of image bytes.

client := &http.Client{
	Timeout: time.Second * 20,
}
body := &bytes.Buffer{}

Multipart Messages

Messages that contain multiple parts like a text message and an image attachment can be encoded with MIME/Multipart, which is also used in e-mails. In this case we will only use one part. We create a multipart writer. The image section of the message with key name „image“ and file name „image.jpg“ is stored in form writer fw. You can choose any name, we are not referring to the filename on disk yet.

writer := multipart.NewWriter(body)
fw, err := writer.CreateFormFile("image", "image.jpg")

Next we open the image from disk using the operating system package os. This file is then copied into the multipart message form writer fw. Any error is logged and then the writer needs to be closed.

buf, err := os.Open("image.jpg")
_, err = io.Copy(fw, buf)
if err != nil {
	log.Fatal(err)
}
writer.Close()

HTTP Post request

Next we create a new http POST request, which in contrast to GET is used to send images and not retrieve them. Here we pass a URL and the bytes buffer that contains the image now and set the appropriate content type header.

req, err := http.NewRequest("POST", "http://127.0.0.1:80/v1/vision/custom/testmodel", bytes.NewReader(body.Bytes()))
req.Header.Set("Content-Type", writer.FormDataContentType())

Then we use the client to perform the request, catch the http status code like 200 for OK or 404 or 400 when there is an issue and store the response in rsp.

HTTP Response

rsp, _ := client.Do(req)

if rsp.StatusCode != http.StatusOK {
	log.Printf("Request failed with response code: %d", rsp.StatusCode)
}
log.Print(rsp.StatusCode)

Then we read the body section of the response and for testing purposes print it to the command line on the server.

body, err := ioutil.ReadAll(rsp.Body)
if err != nil {
	log.Fatal(err)
}
log.Println(string(body))

To return data to the user as JSON we use http response which was passed as an argument in handleRequest function.

w.Header().Set("Content-Type", "JSON")
w.Write(body)

This is now the result of the analysis the microservice has performed on the image, like the name of the detected object and its boundaries.

Conclusion

In this post we loaded an image from disk, send it to a microservice for analysis and provided the response to the user. Missing is the part, where the user can actually send the image himself from the client to the server. If you like the content and want to be notified about any updates please subscribe to the email list.

Full Listing

Here is the full listing. You can also view the code as Go Playground, but unfortunately you can’t execute it (deadlock error). For it to work you need to actually launch the microservice, which we haven’t discussed here.

package main

import (
	"bytes"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"mime/multipart"
	"net/http"
	"os"
	"time"
)

func main() {
	handler := http.HandlerFunc(handleRequest)
	http.Handle("/image", handler)
	fmt.Println("Server startet at port 8080")
	http.ListenAndServe(":5000", nil)
}

func handleRequest(w http.ResponseWriter, r *http.Request) {

	buf, err := os.Open("image.jpg")

	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)
	fw, err := writer.CreateFormFile("image", "image.jpg")

	_, err = io.Copy(fw, buf)
	if err != nil {
		log.Fatal(err)
	}
	writer.Close()

	req, err := http.NewRequest("POST", "http://127.0.0.1:80/v1/vision/custom/testmodel", bytes.NewReader(body.Bytes()))
	req.Header.Set("Content-Type", writer.FormDataContentType())
	client := &http.Client{
		// Set timeout to not be at mercy of microservice to respond and stall the server
		Timeout: time.Second * 20,
	}

	rsp, _ := client.Do(req)

	if rsp.StatusCode != http.StatusOK {
		log.Printf("Request failed with response code: %d", rsp.StatusCode)
	}
	log.Print(rsp.StatusCode)

	body2, err := ioutil.ReadAll(rsp.Body)
	if err != nil {
		log.Fatal(err)
	}
	log.Println(string(body2))

	w.Header().Set("Content-Type", "JSON")
	w.Write(body2)
}

Subscribe to sebastianroy.de

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe