Skip to content

Commit

Permalink
support a multipart body with explicitly set encodings (#2301)
Browse files Browse the repository at this point in the history
support a multipart message containing body parts with explicitly set encodings
  • Loading branch information
flavienbert authored Jul 27, 2023
1 parent ee76d96 commit 77e1c8f
Show file tree
Hide file tree
Showing 5 changed files with 43 additions and 7 deletions.
6 changes: 3 additions & 3 deletions zio-http/src/main/scala/zio/http/FormField.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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

Expand All @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions zio-http/src/main/scala/zio/http/MediaType.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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(";"))))
}
}

Expand Down Expand Up @@ -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 }
}
}
7 changes: 5 additions & 2 deletions zio-http/src/main/scala/zio/http/internal/FormAST.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)}""")
Expand Down
23 changes: 23 additions & 0 deletions zio-http/src/test/scala/zio/http/FormSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
10 changes: 10 additions & 0 deletions zio-http/src/test/scala/zio/http/forms/Fixtures.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down

0 comments on commit 77e1c8f

Please sign in to comment.