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.
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
- Skeleton and imports
- Manipulate binary data from an image
- Multipart Messages
- HTTP Post request
- HTTP Response
- Conclusion
- 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)
}