Sending an Image as POST Request with Swift 5 to Go 1.19 Server
In this post we will upload an image from the user of an iOS app to a server written in Go as an asynchronous multiform POST request.
Table of contents
In this post we will upload an image from the user of an iOS app to a server written in Go as an asynchronous multiform POST request. This could be for uploading a profile image or detecting objects.
We will just return file size, image width and height in pixels as a basic JSON object. If you want to learn how to cast more complex JSON objects into structs, checkout Relaying a Microservice JSON Response to the Client by Unmarshalling Go Structs.
First, we need to obtain the image we intend to send. This could be done by taking a photo using the camera, selecting an image from the library, or loading an image from a URL. Loading an image from a URL is the easiest way. Then we need to create the POST request, send it and catch any error if something went wrong.
If we can’t accept POST requests that contain an image, any request will remain unanswered. So let’s create a server to do that.
Backend in Go 1.19
First initialise a new Go environment and install the echo framework.
$ mkdir ImageHandlerTutorial && cd ImageHandlerTutorial
$ go mod init ImageHandlerTutorial
$ go get github.com/labstack/echo/v4
$ go get github.com/labstack/echo/v4/middleware
Then create a ImageHandlerTutorial.go
file with a basic skeleton for using echo and other imports for writing files or encoding JSON.
package main
import (
"bytes"
// Formating strings
"fmt"
"io"
// Multipart POST request
"mime/multipart"
// Opening and closing files
"os"
"time"
"image"
// Register possible image formats
_ "image/png"
_ "image/jpeg"
"encoding/json"
"io/ioutil"
"log"
"net/http"
// Echo framework
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
e := echo.New()
// Avoid CORS error when testing with redocly
e.Use(middleware.CORS())
// API Versioning
//...
// POST request handling
//...
e.Logger.Fatal(e.Start(":1324"))
}
To provide some structure for the URLs, we create two groups, such that we can create resources under 127.0.0.1:1324/api/v1
. The IP will change if it is running on a real server instead of the same local machine the iOS Simulator will run on later.
// API Versioning
apiGroup := e.Group("/api")
apiv1Group := apiGroup.Group("/v1")
Under this group, we now create an endpoint imageanalysis
. It will handle the POST request 127.0.0.1:1324/api/v1/imageanalysis
.
This also includes a function to handle the image file. We open the data in field file
into the source src
and write the file to dst
on disk.
The way it is written here, different uploads will overwrite the same file name file
. This is a bit sketchy, but also doesn’t use much space. Think about deleting the file after analysis, renaming it if you need it later, and what happens when multiple users call the function at the same time.
apiv1Group.POST("/imageanalysis", func(c echo.Context) error {
// TODO: Catch error when no file is send in POST Request
// Source
file, err := c.FormFile("file")
if err != nil {
return err
}
src, err := file.Open()
if err != nil {
return err
}
defer src.Close()
// Destination
dst, err := os.Create(file.Filename)
if err != nil {
return err
}
defer dst.Close()
// Copy
if _, err = io.Copy(dst, src); err != nil {
return err
}
// TODO: Why do i need to open the file here again, instead of just passing the reference from os.Create()?
// TODO: Maybe don't create the file on disk at all, just send it from memory
buf, err := os.Open(dst.Name())
if err != nil {
log.Fatal(err)
}
defer buf.Close()
return ImageRequestHandler(c, buf)
})
To finish, we start the echo server (but that part is already in the skeleton above)
e.Logger.Fatal(e.Start(":1324"))
Save the file and to run the server open a terminal and execute.
$ go run ImageHandlerTutorial.go
This should output
____ __
/ __/___/ / ___
/ _// __/ _ \/ _ \
/___/\__/_//_/\___/ v4.9.0
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
O\
⇨ http server started on [::]:1324
Go Playground doesn’t support anything with networking, so if you want to try it, you need to install Go on your machine. You can see and download the full listing here.
Returning a JSON with file name, size, width and height
So for practice, let’s return a JSON with the file size in kb and image size in pixels. Here we could also send the image for analysis to another server to do object detection. We would need to perform another POST request, this time from Go to an object detection microservice, and I have written about this in Sending Images in POST request as MultiPart Form from Go to Microservice.
// Send an image to microservice for image analysis
func ImageRequestHandler(c echo.Context, img *os.File) error {
// TODO: Use user image instead
log.Print(img.Name())
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
// TODO: Try to change image name here
fw, err := writer.CreateFormFile("image", "image.jpg")
_, err = io.Copy(fw, img)
if err != nil {
log.Fatal(err)
}
writer.Close()
// Get filesize of image
stat, err := img.Stat()
log.Print("File size: ", stat.Size()/1024, " kb")
// Get width and height in px for the image
im, _, err := image.DecodeConfig(img)
if err != nil {
fmt.Fprintf(os.Stderr, "%s: %v\n", img.Name(), err)
}
log.Print(im.Width)
// Form JSON response that contains name, size, height and width
jsonStr := fmt.Sprintf(`{ "name": "%s",
"size": %d,
"width": %d,
"height": %d
}`, img.Name(), stat.Size(), im.Width, im.Height)
log.Print(jsonStr)
c.Response().Header().Set("Content-Type", "application/json")
//TODO: Avoid double casting []byte string?!
c.Response().Write([]byte(string(jsonStr)))
// TODO: Figure out how to avoid return and just return with c.Response() similar to w.Write
return err
}
Basic concepts
Multiform/data http requests
A POST
request is one type of HTTP request like GET
and PUT
. There are many types of POST
request, which we can specify in the header. There are image/png
or text
for example. For flexibility, however, we will use multiformat/data
. This offers multiple data fields.
HTTP requests have a boundary. It tells the parser that interesting data is coming and when it sees the same boundary again that this is the end of that data batch. We will define it using a random uuidString
. This is just a common way to generate random text to uniquely identify something and can look like 633a76d8-87a3-494c-bd1c-7192985e01f5
.
Asynchronous tasks with DispatchQueues
Like any type of networking activity, it takes some time to execute and might even fail. We want to avoid blocking the user interface waiting for a response. That’s why multithreading is used. This is a way to make asynchronous networking requests. There are many ways to do this and it is a complex subject. Here we will use DispatchQueues
. This is a way to schedule tasks with the first-in-first-out principle (FIFO) and we let iOS handle thread management.
Swift Code Skeleton
We will use two functions loadFrom
and uploadImage
and wrap those up in a class POSTImage
. Then we create an object postRequest
of our new type POSTImage
and execute.
import UIKit
class POSTImage {
var image = UIImage()
// Load an image from URL
func loadFromURL(URLAddress: String) {
}
// Load an image from Library (TODO)
func loadImageFromLibrary() {}
// Sending the image to the server
func uploadImage(fileName: String, image: UIImage) {
}
}
let postRequest = POSTImage()
postRequest.loadFromURL(URLAddress: "https://upload.wikimedia.org/wikipedia/commons/b/b9/Panther_chameleon_%28Furcifer_pardalis%29_male_Montagne_d%E2%80%99Ambre.jpg")
Selecting an Image
Load an image from URL
So, now let’s get an image by loading it from a URL with loadFromURL(...)
with an URLAddress
of type String
as argument. We specified the URL in the last line of our skeleton above already. In a real scenario, this could be using a URL from a s3 object storage. We use guard
to catch errors when parsing the URL.
guard let url = URL(string: URLAddress) else {
return
}
Next we will load the image. Loading an image takes some time and can fail. We would rather not block the user interface, therefore handling the task over to a separate thread with DispatchQueue.main.async
. First, we try to obtain the Data
from the url
into imageData
. Then the imageData
will be cast into loadedImage
which can be used for display as UIImage
.
Now we will upload the image. This is a provisional solution, but it works. The issue is that we loaded the image in a separate thread. We can only call uploadImage
, once we already got the image. Therefore, our program has to wait or we need a callback function that is called after the image was loaded from URL. We will do that later, but for now, we just call uploadImage
from the sub thread in DispatchQueue
.
DispatchQueue.main.async { [weak self] in
if let imageData = try? Data(contentsOf: url) {
if let loadedImage = UIImage(data: imageData) {
self?.image = loadedImage
// TODO: Remove this and use completion handler instead
self?.uploadImage(fileName: "file", image: loadedImage)
}
}
The other issue is that we haven’t implemented uploadImage
yet, which we will do next. Here is the full listing for the function selectImageFromURL
.
func selectImageFromURL(URLAddress: String) {
guard let url = URL(string: URLAddress) else {
return
}
DispatchQueue.main.async { [weak self] in
if let imageData = try? Data(contentsOf: url) {
if let loadedImage = UIImage(data: imageData) {
self?.image = loadedImage
// TODO: Remove this and use completion handler instead
self?.uploadImage(fileName: "file", image: loadedImage)
}
}
Create and send POST request
Now we create a URLSession named session
. It will be sent to the url
specified by URL(...)
. This must be an endpoint to your server, that will handle HTTP POST requests. I have written a previous post about how to do this also with URLSession
and DispatchQueues
at least with JSON objects (text, no images) in Communications from Go Backend to SwiftUI Frontend with JSON API.
First, we need to define the server URL and start a URLSession to handle networking.
let url = URL(string: "http://127.0.0.1:1324/api/v1/imageanalysis")
let session = URLSession.shared
Now we configure the request as POST
request and making it a multipart/form-data
request in the header, where we configure how the boundary
looks like.
let boundary = UUID().uuidString
var urlRequest = URLRequest(url: url!)
urlRequest.httpMethod = "POST"
urlRequest.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
After the header we need to set the actual data. We create data
of type Data
. Then we append the boundary and add the parameter ”file”. The Content-Type
of this parameter is image/png
. And finally we have to append the actual file data image.pngData
. Finally, we close the requests boundary.
var data = Data()
// Add the image data to the raw http request data
data.append("\r\n--\(boundary)\r\n".data(using: .utf8)!)
let paramName = "file"
data.append("Content-Disposition: form-data; name=\"\(paramName)\"; filename=\"\(fileName)\"\r\n".data(using: .utf8)!)
data.append("Content-Type: image/png\r\n\r\n".data(using: .utf8)!)
data.append(image.pngData()!)
data.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
Now that we defined the request, it is time to execute it. We use the session.uploadTask
with our created urlRequest
and data
as arguments. Then we handle the response in the completionHandler
. There we convert the JSON response into a Go struct.
session.uploadTask(with: urlRequest, from: data, completionHandler: { responseData, response, error in
if error == nil {
let jsonData = try?JSONSerialization.jsonObject(with: responseData!, options: .allowFragments)
if let json = jsonData as? [String: Any] {
print(json)
}
}
}).resume()
Now let’s look at the full listing of the function uploadImage
.
func uploadImage(fileName: String, image: UIImage) {
let url = URL(string: "http://127.0.0.1:1324/api/v1/electrophoresis/imageanalysis")
let boundary = UUID().uuidString
let session = URLSession.shared
var urlRequest = URLRequest(url: url!)
urlRequest.httpMethod = "POST"
urlRequest.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
var data = Data()
// Add the image data to the raw http request data
data.append("\r\n--\(boundary)\r\n".data(using: .utf8)!)
let paramName = "file"
data.append("Content-Disposition: form-data; name=\"\(paramName)\"; filename=\"\(fileName)\"\r\n".data(using: .utf8)!)
data.append("Content-Type: image/png\r\n\r\n".data(using: .utf8)!)
data.append(image.pngData()!)
data.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
session.uploadTask(with: urlRequest, from: data, completionHandler: { responseData, response, error in
if error == nil {
let jsonData = try? JSONSerialization.jsonObject(with: responseData!, options: .allowFragments)
if let json = jsonData as? [String: Any] {
print(json)
}
}
}).resume()
}
Finally, we get to see the result of all the hard work so far:
["size": 5451466, "name": file, "height": 1396, "width": 2872]
This is the analysis done by the server, shown on the client. Real world analysis would be more complex and the client would display that data on the UI instead of just printing it to the console.
Bonus: Some curious errors
One issue I had was that data function wasn’t found. This was because I created my own data type that was called data
. This has overridden the native function. So give your types new names that are not used in iOS. iOS libraries prefix any new class or struct to avoid it.
Full Listing
import UIKit
class POSTImage {
var image = UIImage()
func loadFrom(URLAddress: String) {
guard let url = URL(string: URLAddress) else {
return
}
DispatchQueue.main.async { [weak self] in
if let imageData = try? Data(contentsOf: url) {
if let loadedImage = UIImage(data: imageData) {
// Why is self optional here?
self?.image = loadedImage
self?.uploadImage(fileName: "file", image: loadedImage)
}
}
}
}
func uploadImage(fileName: String, image: UIImage) {
let url = URL(string: "http://127.0.0.1:1324/api/v1/imageanalysis")
let boundary = UUID().uuidString
let session = URLSession.shared
var urlRequest = URLRequest(url: url!)
urlRequest.httpMethod = "POST"
urlRequest.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
var data = Data()
// Add the image data to the raw http request data
data.append("\r\n--\(boundary)\r\n".data(using: .utf8)!)
let paramName = "file"
data.append("Content-Disposition: form-data; name=\"\(paramName)\"; filename=\"\(fileName)\"\r\n".data(using: .utf8)!)
data.append("Content-Type: image/png\r\n\r\n".data(using: .utf8)!)
data.append(image.pngData()!)
data.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
session.uploadTask(with: urlRequest, from: data, completionHandler: { responseData, response, error in
if error == nil {
let jsonData = try? JSONSerialization.jsonObject(with: responseData!, options: .allowFragments)
if let json = jsonData as? [String: Any] {
print(json)
}
}
}).resume()
}
}
let postRequest = POSTImage()
postRequest.loadFrom(URLAddress: "https://upload.wikimedia.org/wikipedia/commons/b/b9/Panther_chameleon_%28Furcifer_pardalis%29_male_Montagne_d%E2%80%99Ambre.jpg")
// TODO: wait for myimageview to load
// uploadImage(fileName: "file", image: myImageView.image)
Conclusion
To sum things up, we discussed the server and client side of sending images over HTTP with POST requests, doing simple analysis on the server and returning the result as JSON to the client. I hope I could cover this subject in a useful way with most of the puzzle pieces.
Links
https://orjpap.github.io/swift/http/ios/urlsession/2021/04/26/Multipart-Form-Requests.html https://stackoverflow.com/questions/29623187/upload-image-with-multipart-form-data-ios-in-swift https://www.hackingwithswift.com/books/ios-swiftui/sending-and-receiving-orders-over-the-internet