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.

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. 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.

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