diff --git a/zio-http/src/main/scala/zio/http/FormField.scala b/zio-http/src/main/scala/zio/http/FormField.scala index ce880129b7..3d059f9a3a 100644 --- a/zio-http/src/main/scala/zio/http/FormField.scala +++ b/zio-http/src/main/scala/zio/http/FormField.scala @@ -175,7 +175,7 @@ object FormField { contentParts = extract._4.tail // Skip the first empty line content = contentParts.foldLeft(Chunk.empty[Byte])(_ ++ _.bytes) contentType = extract._2 - .flatMap(x => MediaType.forContentType(x.preposition)) + .flatMap(x => MediaType.forContentType(x.value)) .getOrElse(MediaType.application.`octet-stream`) transferEncoding = extract._3 .flatMap(x => ContentTransferEncoding.parse(x.preposition).toOption) @@ -190,7 +190,7 @@ object FormField { ast.collectFirst { case header: FormAST.Header if header.name == "Content-Type" => MediaType - .forContentType(header.preposition) + .forContentType(header.value) .getOrElse(MediaType.application.`octet-stream`) // Unknown content type defaults to binary }.getOrElse(MediaType.text.plain) // Missing content type defaults to text @@ -213,7 +213,7 @@ object FormField { disposition <- ZIO.fromOption(extract._1).orElseFail(FormDataMissingContentDisposition) name <- ZIO.fromOption(extract._1.flatMap(_.fields.get("name"))).orElseFail(ContentDispositionMissingName) contentType = extract._2 - .flatMap(x => MediaType.forContentType(x.preposition)) + .flatMap(x => MediaType.forContentType(x.value)) .getOrElse(MediaType.text.plain) transferEncoding = extract._3 .flatMap(x => ContentTransferEncoding.parse(x.preposition).toOption) diff --git a/zio-http/src/main/scala/zio/http/MediaType.scala b/zio-http/src/main/scala/zio/http/MediaType.scala index 48a54a8775..70390e5a36 100644 --- a/zio-http/src/main/scala/zio/http/MediaType.scala +++ b/zio-http/src/main/scala/zio/http/MediaType.scala @@ -42,7 +42,7 @@ object MediaType extends MediaTypes { val (contentType1, parameter) = contentType.splitAt(index) contentTypeMap .get(contentType1) - .map(_.copy(parameters = parseOptionalParameters(parameter.tail.split(";")))) + .map(_.copy(parameters = parseOptionalParameters(parameter.split(";")))) } } @@ -76,6 +76,6 @@ object MediaType extends MediaTypes { case _ => parameterMap } - loop(parameters.toIndexedSeq, Map.empty) + loop(parameters.toIndexedSeq, Map.empty).map { case (key, value) => key.trim -> value.trim } } } diff --git a/zio-http/src/main/scala/zio/http/internal/FormAST.scala b/zio-http/src/main/scala/zio/http/internal/FormAST.scala index d86cc51fd1..ac21d6ecfb 100644 --- a/zio-http/src/main/scala/zio/http/internal/FormAST.scala +++ b/zio-http/src/main/scala/zio/http/internal/FormAST.scala @@ -108,8 +108,11 @@ private[http] object FormAST { private def makeField(name: String, value: Option[String]): String = value.map(makeField(name, _)).getOrElse("") - def contentType(contentType: MediaType, charset: Option[Charset] = None): Header = - Header("Content-Type", s"${contentType.fullType}${makeField("charset", charset.map(_.name))}") + def contentType(contentType: MediaType): Header = + Header( + "Content-Type", + s"${contentType.fullType}${contentType.parameters.map { case (name, value) => s"; $name=$value" }.mkString("")}", + ) def contentDisposition(name: String, filename: Option[String] = None): Header = Header("Content-Disposition", s"""form-data${makeField("name", name)}${makeField("filename", filename)}""") diff --git a/zio-http/src/test/scala/zio/http/FormSpec.scala b/zio-http/src/test/scala/zio/http/FormSpec.scala index d256f2210a..b66a884387 100644 --- a/zio-http/src/test/scala/zio/http/FormSpec.scala +++ b/zio-http/src/test/scala/zio/http/FormSpec.scala @@ -85,6 +85,29 @@ object FormSpec extends ZIOSpecDefault { form2 == form, ) }, + test("encoding with custom paramaters [charset]") { + + val form = Form( + FormField.textField( + "csv-data", + "foo,bar,baz", + MediaType.text.csv.copy(parameters = Map("charset" -> "UTF-8")), + ), + ) + + val actualByteStream = form.multipartBytes(Boundary("(((AaB03x)))")) + + def stringify(bytes: Chunk[Byte]): String = + new String(bytes.toArray, StandardCharsets.UTF_8) + + for { + form4 <- Form.fromMultipartBytes(multipartFormBytes4) + actualBytes <- actualByteStream.runCollect + } yield assertTrue( + stringify(actualBytes) == stringify(multipartFormBytes4), + form4.formData.head.contentType == form.formData.head.contentType, + ) + }, test("decoding") { val boundary = Boundary("AaB03x") diff --git a/zio-http/src/test/scala/zio/http/forms/Fixtures.scala b/zio-http/src/test/scala/zio/http/forms/Fixtures.scala index 8b42d7d976..0c8de402ea 100644 --- a/zio-http/src/test/scala/zio/http/forms/Fixtures.scala +++ b/zio-http/src/test/scala/zio/http/forms/Fixtures.scala @@ -89,6 +89,16 @@ object Fixtures { |""".stripMargin.getBytes(), ) + val multipartFormBytes4 = + Chunk.fromArray( + s"""|--(((AaB03x)))${CR} + |Content-Disposition: form-data; name="csv-data"${CR} + |Content-Type: text/csv; charset=UTF-8${CR} + |${CR} + |foo,bar,baz${CR} + |--(((AaB03x)))--${CRLF}""".stripMargin.getBytes(), + ) + private def simpleFormField: Gen[Any, (FormField, Schema[Any], Option[String], Boolean)] = for { name <- Gen.option(Gen.string1(Gen.alphaNumericChar))