From c722ec2e1b7a8d3c5df2efd3eb8931e184ac333b Mon Sep 17 00:00:00 2001 From: nk Date: Wed, 13 Nov 2024 00:52:49 +0200 Subject: [PATCH 1/2] Fixes form submissions with empty file fields throwing exceptions --- build.mill | 1 + cask/src/cask/model/Params.scala | 18 ++++++--- .../app/resources/example.txt | 0 .../app/src/MultipartFormSubmission.scala | 26 +++++++++++++ .../app/test/src/ExampleTests.scala | 38 +++++++++++++++++++ example/multipartFormSubmission/package.mill | 18 +++++++++ 6 files changed, 96 insertions(+), 5 deletions(-) create mode 100644 example/multipartFormSubmission/app/resources/example.txt create mode 100644 example/multipartFormSubmission/app/src/MultipartFormSubmission.scala create mode 100644 example/multipartFormSubmission/app/test/src/ExampleTests.scala create mode 100644 example/multipartFormSubmission/package.mill diff --git a/build.mill b/build.mill index 95013993ad..1d2a8798d8 100644 --- a/build.mill +++ b/build.mill @@ -125,6 +125,7 @@ def zippedExamples = T { build.example.websockets2.millSourcePath, build.example.websockets3.millSourcePath, build.example.websockets4.millSourcePath, + build.example.multipartFormSubmission.millSourcePath, ) for (example <- examples) yield { diff --git a/cask/src/cask/model/Params.scala b/cask/src/cask/model/Params.scala index 09871e57b4..6f3fba610c 100644 --- a/cask/src/cask/model/Params.scala +++ b/cask/src/cask/model/Params.scala @@ -1,10 +1,12 @@ package cask.model import java.io.{ByteArrayOutputStream, InputStream} - import cask.internal.Util import io.undertow.server.HttpServerExchange import io.undertow.server.handlers.CookieImpl +import io.undertow.util.HttpString +import scala.util.Try +import scala.collection.JavaConverters.collectionAsScalaIterableConverter case class QueryParams(value: Map[String, collection.Seq[String]]) case class RemainingPathSegments(value: Seq[String]) @@ -98,9 +100,13 @@ sealed trait FormEntry{ } } object FormEntry{ - def fromUndertow(from: io.undertow.server.handlers.form.FormData.FormValue) = { - if (!from.isFile) FormValue(from.getValue, from.getHeaders) - else FormFile(from.getFileName, from.getPath, from.getHeaders) + def fromUndertow(from: io.undertow.server.handlers.form.FormData.FormValue): FormEntry = { + val isOctetStream = Option(from.getHeaders) + .flatMap(headers => Option(headers.get(HttpString.tryFromString("Content-Type")))) + .exists(h => h.asScala.exists(v => v == "application/octet-stream")) + // browsers will set empty file fields to content type: octet-stream + if (isOctetStream || from.isFileItem) FormFile(from.getFileName, Try(from.getFileItem.getFile).toOption, from.getHeaders) + else FormValue(from.getValue, from.getHeaders) } } @@ -110,7 +116,9 @@ case class FormValue(value: String, } case class FormFile(fileName: String, - filePath: java.nio.file.Path, + filePath: Option[java.nio.file.Path], headers: io.undertow.util.HeaderMap) extends FormEntry{ def valueOrFileName = fileName } + +case class EmptyFormEntry() diff --git a/example/multipartFormSubmission/app/resources/example.txt b/example/multipartFormSubmission/app/resources/example.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/example/multipartFormSubmission/app/src/MultipartFormSubmission.scala b/example/multipartFormSubmission/app/src/MultipartFormSubmission.scala new file mode 100644 index 0000000000..f67ded041c --- /dev/null +++ b/example/multipartFormSubmission/app/src/MultipartFormSubmission.scala @@ -0,0 +1,26 @@ +package app + +object MultipartFormSubmission extends cask.MainRoutes { + + @cask.get("/") + def index() = + cask.model.Response( + """ + + + + +
+ + +
+ + + """, 200, Seq(("Content-Type", "text/html"))) + + @cask.postForm("/post") + def post(somefile: cask.FormFile) = + s"filename: ${somefile.fileName}" + + initialize() +} diff --git a/example/multipartFormSubmission/app/test/src/ExampleTests.scala b/example/multipartFormSubmission/app/test/src/ExampleTests.scala new file mode 100644 index 0000000000..ebe8857a37 --- /dev/null +++ b/example/multipartFormSubmission/app/test/src/ExampleTests.scala @@ -0,0 +1,38 @@ +package app +import io.undertow.Undertow + +import utest._ + +object ExampleTests extends TestSuite{ + def withServer[T](example: cask.main.Main)(f: String => T): T = { + val server = Undertow.builder + .addHttpListener(8081, "localhost") + .setHandler(example.defaultHandler) + .build + server.start() + val res = + try f("http://localhost:8081") + finally server.stop() + res + } + + val tests = Tests{ + test("MultipartFormSubmission") - withServer(MultipartFormSubmission){ host => + val classPath = System.getProperty("java.class.path", "."); + val elements = classPath.split(System.getProperty("path.separator")); + elements.filter(e => e.endsWith("/app/resources")).headOption.map(resourcePath => { + val withFile = requests.post(s"$host/post", data = requests.MultiPart( + requests.MultiItem("somefile", new java.io.File(s"$resourcePath/example.txt"), "example.txt"), + )) + withFile.text() ==> s"filename: example.txt" + withFile.statusCode ==> 200 + + val withoutFile = requests.post(s"$host/post", data = requests.MultiPart( + requests.MultiItem("somefile", Array[Byte]()), + )) + withoutFile.text() ==> s"filename: null" + withoutFile.statusCode ==> 200 + }).isDefined ==> true + } + } +} diff --git a/example/multipartFormSubmission/package.mill b/example/multipartFormSubmission/package.mill new file mode 100644 index 0000000000..1908ce5236 --- /dev/null +++ b/example/multipartFormSubmission/package.mill @@ -0,0 +1,18 @@ +package build.example.multipartFormSubmission +import mill._, scalalib._ + +object app extends Cross[AppModule](build.scalaVersions) +trait AppModule extends CrossScalaModule{ + + def moduleDeps = Seq(build.cask(crossScalaVersion)) + + def ivyDeps = Agg[Dep]( + ) + object test extends ScalaTests with TestModule.Utest{ + + def ivyDeps = Agg( + ivy"com.lihaoyi::utest::0.8.4", + ivy"com.lihaoyi::requests::0.9.0", + ) + } +} From ada2090d7409e8ad547918a0cebca64eb3cd2d67 Mon Sep 17 00:00:00 2001 From: nk Date: Wed, 13 Nov 2024 22:11:36 +0200 Subject: [PATCH 2/2] Fixes resource file not available in github test action --- .../app/test/src/ExampleTests.scala | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/example/multipartFormSubmission/app/test/src/ExampleTests.scala b/example/multipartFormSubmission/app/test/src/ExampleTests.scala index ebe8857a37..9c065ead09 100644 --- a/example/multipartFormSubmission/app/test/src/ExampleTests.scala +++ b/example/multipartFormSubmission/app/test/src/ExampleTests.scala @@ -16,23 +16,19 @@ object ExampleTests extends TestSuite{ res } - val tests = Tests{ - test("MultipartFormSubmission") - withServer(MultipartFormSubmission){ host => - val classPath = System.getProperty("java.class.path", "."); - val elements = classPath.split(System.getProperty("path.separator")); - elements.filter(e => e.endsWith("/app/resources")).headOption.map(resourcePath => { - val withFile = requests.post(s"$host/post", data = requests.MultiPart( - requests.MultiItem("somefile", new java.io.File(s"$resourcePath/example.txt"), "example.txt"), - )) - withFile.text() ==> s"filename: example.txt" - withFile.statusCode ==> 200 + val tests = Tests { + test("MultipartFormSubmission") - withServer(MultipartFormSubmission) { host => + val withFile = requests.post(s"$host/post", data = requests.MultiPart( + requests.MultiItem("somefile", Array[Byte](1,2,3,4,5) , "example.txt"), + )) + withFile.text() ==> s"filename: example.txt" + withFile.statusCode ==> 200 - val withoutFile = requests.post(s"$host/post", data = requests.MultiPart( - requests.MultiItem("somefile", Array[Byte]()), - )) - withoutFile.text() ==> s"filename: null" - withoutFile.statusCode ==> 200 - }).isDefined ==> true + val withoutFile = requests.post(s"$host/post", data = requests.MultiPart( + requests.MultiItem("somefile", Array[Byte]()), + )) + withoutFile.text() ==> s"filename: null" + withoutFile.statusCode ==> 200 } } }