zio / zio-http

A next-generation Scala framework for building scalable, correct, and efficient HTTP clients and servers
https://zio.dev/zio-http
Apache License 2.0
787 stars 396 forks source link

CRLF stripped in TextFields inside Multipart request #2976

Closed narma closed 2 months ago

narma commented 2 months ago

zio-http unexpectedly stripped CRLF inside TextField parts in multipart/form-data requests. We are using config with request streaming enabled, other settings are default. It happens with actual csv files which are uploaded via browser on our production code.

Given snippet reproducing the issue

//> using dep "dev.zio::zio:2.1.6"
//> using dep "dev.zio::zio-streams:2.1.6"
//> using dep "dev.zio::zio-http:3.0.0-RC9"

import zio.*
import zio.http.*

object ZioHttpIssue extends zio.ZIOAppDefault:

  def myhandler = handler { (req: Request) => 
      for {
        form <- req.body.asMultipartFormStream
        data <- form.fields.mapZIO { field =>
          field.asChunk
        }.runCollect
        text = data.head
      } yield text.asString
  }

  def mkBody(csv: String) = Body.fromMultipartFormUUID(
                      Form(
                        FormField.textField(
                          name = "csv_here",
                          value = csv,
                          mediaType = MediaType.forContentType("text/csv").get
                        )
                      )
                    )
  def run = {
    val dataCRLF = "1,2\r\n3,4\r\n"
    val dataLF = "1,2\n3,4\n"
    for {
      bodyCRLF <- mkBody(dataCRLF)
      bodyLF <- mkBody(dataLF)
      resultCRLF <- myhandler(Request.post("/", bodyCRLF))
      resultLF <- myhandler(Request.post("/", bodyLF))
      _ <- ZIO.whenDiscard(resultCRLF != dataCRLF) { 
        ZIO.succeed(println(s"For \\r\\n in body we got stripped it, given $resultCRLF, expected: $dataCRLF"))
      }
      _ <- if (resultLF == dataLF) 
        ZIO.succeed(println(s"For \\n in body we are good."))
        else ZIO.fail(new RuntimeException("For \\n in body we are not good too."))

      _ <- ZIO.fail(new RuntimeException("assertTrue(resultCRLF == dataCRLF)")).when(resultCRLF != dataCRLF)
    } yield ()
  }
narma commented 2 months ago

Test case for an issue

test("crlf not stripped") {
  val content = "1,2\r\n3,4\r\n"
  val form = Form(Chunk(
    FormField.textField("csv", content, MediaType.text.`csv`)
  ))
  val boundary       = Boundary("X-INSOMNIA-BOUNDARY")
  val formByteStream = form.multipartBytes(boundary)
  val streamingForm  = StreamingForm(formByteStream, boundary)
  streamingForm.fields.mapZIO { field =>
    field.asChunk
  }.runCollect
    .map { chunks =>
      val s = chunks.headOption.map(_.asString)
      assertTrue(s.is(_.some).size == content.size, s.is(_.some) == content)
    }
}
jdegoes commented 2 months ago

/bounty $150 for fix and test case to prevent regression.

algora-pbc[bot] commented 2 months ago

💎 $150 bounty • ZIO

Steps to solve:

  1. Start working: Comment /attempt #2976 with your implementation plan
  2. Submit work: Create a pull request including /claim #2976 in the PR body to claim the bounty
  3. Receive payment: 100% of the bounty is received 2-5 days post-reward. Make sure you are eligible for payouts

Thank you for contributing to zio/zio-http!

Add a bountyShare on socials

Attempt Started (GMT+0) Solution
🟢 @987Nabil #2983
algora-pbc[bot] commented 2 months ago

💡 @987Nabil submitted a pull request that claims the bounty. You can visit your bounty board to reward.

algora-pbc[bot] commented 2 months ago

🎉🎈 @987Nabil has been awarded $150! 🎈🎊