upload image from ktor client to ktor server

Uploading image using Android Ktor client to Ktor server

Level: Medium

Ktor is an excellent client for api requests in Android, it has many features like web-socket support, asynchronous HTTP requests, and you can extend its functionality with plugins such as authentication and JSON serialization, however uploading an image with a Ktor cleint to a Ktor server wasn’t similar to Retrofit, as trying to upload the image as a MultipartBody i will get an error “Content-Type header is required for multipart processing” if i add the header i get “Header Content-Type is controlled by the engine and cannot be set explicitly” so here is a solution that has worked for me.

Getting the image:

This example will show how to pick an image in a jetpack compose project in android, first lets create a launcher, it will be used to lunch the image picker activity which allows the user to pick an image from their phone, once the user picks an image we will have a Uri that we can use to read the image in the next step

val context = LocalContext.current
val imageData = remember { mutableStateOf<Uri?>(null) }
val launcher = rememberLauncherForActivityResult(
        contract = (ActivityResultContracts.GetContent()),
        onResult = { uri ->
            imageData.value = uri
        }
    )

to trigger the image picker activity add the following line in your button click (or whenever you need to)

launcher.launch("image/jpeg")

Sending the image with Ktor Client:

now that we have the image Uri we need to open an input stream and pass the Uri to start reading the byte array of the image file

val inputStream = context.contentResolver.openInputStream(it)
val imageByteArray = inputStream?.readBytes()
uploadImage(imageByteArray)

now lets implement our uploadImage function which will use the Ktor client to send the image to our server, please note that the “text” parameter here is optional and is just an example of how to send other parameters in the same request

suspend fun uploadImage(text: String, byteArray: ByteArray?): Boolean {
        return try {
            if (byteArray != null) {
                val response: HttpResponse = client.submitFormWithBinaryData(
                    url = ImageService.Endpoints.AddImage.url,
                    formData = formData {
                        //the line below is just an example of how to send other parameters in the same request
                        append("text", text)
                        append("image", byteArray, Headers.build {
                        append(HttpHeaders.ContentType, "image/jpeg")
                        append(HttpHeaders.ContentDisposition, "filename=image.png")
                        })
                    }
                ) {
                    onUpload { bytesSentTotal, contentLength ->
                        println("Sent $bytesSentTotal bytes from $contentLength")
                    }
                }
            }
            true
        } catch (ex: Exception) {
//im using timber for logs, you can always replace this with Log.d
            Timber.d("error ${ex.message}")
            false
        }
    }

the above function will return true if the upload was successful or false if there was an error, we can also track the upload progress using the onUpload block

Receiving the image in Ktor server:

create a post request to process your request and save the image

post("/add-image") {
            val multipart = call.receiveMultipart()
            var fileName: String? = null
            var text: String?= null
            try{
                multipart.forEachPart { partData ->
                    when(partData){
                        is PartData.FormItem -> {
                            //to read additional parameters that we sent with the image
                            if (partData.name == "text"){
                                text = partData.value
                            }
                        }
                        is PartData.FileItem ->{
                            fileName = partData.save(Constants.USER_IMAGES_PATH)
                        }
                        is PartData.BinaryItem -> Unit
                    }
                }

                val imageUrl = "${Constants.BASE_URL}/uploaded_images/$fileName"
                call.respond(HttpStatusCode.OK,imageUrl)
            } catch (ex: Exception) {
                File("${Constants.USER_IMAGES_PATH}/$fileName").delete()
                call.respond(HttpStatusCode.InternalServerError,"Error")
            }
        }

create the save extension function that is used in the code above, this function creates a random name for our file, attaches the correct file extension, saves the file to our path, writes the image byte data to the newly created file then return its file name

fun PartData.FileItem.save(path: String): String {
    val fileBytes = streamProvider().readBytes()
    val fileExtension = originalFileName?.takeLastWhile { it != '.' }
    val fileName = UUID.randomUUID().toString() + "." + fileExtension
    val folder = File(path)
    folder.mkdir()
    println("Path = $path $fileName")
    File("$path$fileName").writeBytes(fileBytes)
    return fileName
}

That’s it thanks for reading ! please feel free to post your questions or suggestions to improve the article in the comments.

5 3 votes
Article Rating
Subscribe
Notify of
guest

8 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Digital

thanks for the article if you have more video or sources to explain that in more detail. and thanks again

Sam

Thanks Taha for the articles. Can you give any hints about if I wanted to upload an image as part of a data class, like with the fruits, as I am familiar with as a beginner? e.g. I have a name, email and want this image appended to that?

Sam

thanks for the reply, looking forward to the article.

Sam

Taha – I have a lot of what are probably stupid questions about how this might look on the side of the compose project (for this example and for things a little more complicated) – possibly such that it would be too much to expect you to answer here, unless you have an article planned. I thought about contacting you to see what’s possible but your contact page doesn’t seem to be working. Let me know what’s best when you’re free. Thanks.

Pavel

Serializer for class ‘MultiPartFormDataContent’ is not found. Mark the class as @Serializable or provide the serializer explicitly. Rewrote your code and getting this error. Maybe you need to register something in the ktor client?