A basic application to handle multipart form data using akka-http with test cases in Scala


In my previous blogs, I have talked about file upload and its test cases using akka-http.

Now, in this blog I am going to explain the handling of multi part form data in akka-http with the help of a basic application which contains the code and its test cases.

So Let’s start with the dependencies for the application.
You have to add the following dependencies in you build.sbt :

 val akkaV = "2.4.10"
 val scalaTestV = "3.0.0"
 Seq(
 "com.typesafe.akka" %% "akka-actor" % akkaV,
 "com.typesafe.akka" %% "akka-http-experimental" % akkaV,
 "com.typesafe.akka" %% "akka-http-testkit" % akkaV,
 "org.scalatest" %% "scalatest" % scalaTestV % "test"
 )

Now, we have to create a handler to handle multipart form data :

package com.knoldus

import java.io.File

import akka.actor.ActorSystem
import akka.http.scaladsl.model.Multipart.BodyPart
import akka.http.scaladsl.model.{HttpResponse, Multipart, StatusCodes}
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import akka.stream.Materializer
import akka.stream.scaladsl.FileIO

import scala.concurrent.duration._
import scala.concurrent.{ExecutionContextExecutor, Future}

trait MultipartFormDataHandler {

implicit val system: ActorSystem

implicit def executor: ExecutionContextExecutor

implicit val materializer: Materializer

val routes = processMultiPartData

def processMultiPartData: Route = path("process" / "multipart" / "data") {
(post & entity(as[Multipart.FormData])) { formData =>
complete {
val extractedData: Future[Map[String, Any]] = formData.parts.mapAsync[(String, Any)](1) {

case file: BodyPart if file.name == "file" => val tempFile = File.createTempFile("process", "file")
file.entity.dataBytes.runWith(FileIO.toPath(tempFile.toPath)).map { ioResult=>
s"file ${file.filename.fold("Unknown")(identity)}" -> s"${ioResult.count} bytes"}

case data: BodyPart => data.toStrict(2.seconds).map(strict =>data.name -> strict.entity.data.utf8String)
}.runFold(Map.empty[String, Any])((map, tuple) => map + tuple)

extractedData.map { data => HttpResponse(StatusCodes.OK, entity = s"Data : ${data.mkString(", ")} has been successfully saved.")}
.recover {
case ex: Exception =>HttpResponse(StatusCodes.InternalServerError,entity = s"Error in processing multipart form data due to ${ex.getMessage}")
}
}
}
}
}

Whenever a file will come in request, a new file will be created in temp directory of your system.
Below, you can find the test cases for the handler :

package com.knoldus

import java.io.File

import akka.http.scaladsl.model.{ContentTypes, HttpEntity, Multipart, StatusCodes}
import akka.http.scaladsl.testkit.ScalatestRouteTest
import org.scalatest.{FlatSpec, Matchers}

class MultipartFormDataHandlerSpec extends FlatSpec with Matchers with ScalatestRouteTest with MultipartFormDataHandler {
override def testConfigSource = "akka.loglevel = WARNING"

val firstName = Multipart.FormData.BodyPart.Strict("FirstName", "Rishi")
val lastName = Multipart.FormData.BodyPart.Strict("LastName", "Khandelwal")

"MultiPart Data Handler" should "be able to save multipart data when file has invalid key" in {
val fileData = Multipart.FormData.BodyPart.Strict("invalid", HttpEntity(ContentTypes.`text/plain(UTF-8)`, "This is test file"), Map("fileName" -> "rishi.txt"))
val expectedOutput = Map("FirstName" -> "Rishi", "LastName" -> "Khandelwal", "invalid" -> "This is test file")
val formData = Multipart.FormData(firstName, lastName, fileData)
Post(s"/process/multipart/data", formData) ~> routes ~> check {
status shouldBe StatusCodes.OK
responseAs[String] shouldBe s"""Data : ${expectedOutput.mkString(", ")} has been successfully saved."""
}
}

it should "be able to save multipart data when filename is not given" in {
val fileData = Multipart.FormData.BodyPart.Strict("file", HttpEntity(ContentTypes.`text/plain(UTF-8)`, "This is test file"), Map())
val expectedOutput = Map("FirstName" -> "Rishi", "LastName" -> "Khandelwal", "file Unknown" -> "17 bytes")
val formData = Multipart.FormData(firstName, lastName, fileData)
Post(s"/process/multipart/data", formData) ~> routes ~> check {
status shouldBe StatusCodes.OK
responseAs[String] shouldBe s"""Data : ${expectedOutput.mkString(", ")} has been successfully saved."""
}
}

it should "be able to save multipart data when file content is provided" in {
val fileData = Multipart.FormData.BodyPart.Strict("file", HttpEntity(ContentTypes.`text/plain(UTF-8)`, "This is test file"), Map("fileName" -> "rishi.txt"))
val expectedOutput = Map("FirstName" -> "Rishi", "LastName" -> "Khandelwal", "file rishi.txt" -> "17 bytes")
val formData = Multipart.FormData(firstName, lastName, fileData)
Post(s"/process/multipart/data", formData) ~> routes ~> check {
status shouldBe StatusCodes.OK
responseAs[String] shouldBe s"""Data : ${expectedOutput.mkString(", ")} has been successfully saved."""
}
}

it should "be able to save multipart data when file is read from given path" in {
val fileData = Multipart.FormData.BodyPart.fromPath("file", ContentTypes.`text/plain(UTF-8)`, new File("./README.md").toPath)
val expectedOutput = Map("FirstName" -> "Rishi", "LastName" -> "Khandelwal", "file README.md" -> "414 bytes")
val formData = Multipart.FormData(firstName, lastName, fileData)
Post(s"/process/multipart/data", formData) ~> routes ~> check {
status shouldBe StatusCodes.OK
responseAs[String] shouldBe s"""Data : ${expectedOutput.mkString(", ")} has been successfully saved."""
}
}
}

I hope, you enjoyed it and it will be helpful for you.

You can get full code here

Happy Blogging !!!

About Rishi Khandelwal

Sr. Software Engineer having more than 5 years industry experience. He has working experience in various technologies such as Scala, Java, Play, Akka, Lift Web, Spark, ElasticSearch, Backbone.js, html5, javascript, Less, Amazon EC2, WebRTC, SBT
This entry was posted in Akka, Scala. Bookmark the permalink.

2 Responses to A basic application to handle multipart form data using akka-http with test cases in Scala

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s