ajaxscalaposthttp4s

Ajax POST request with Array[Byte] response


I'm receiving an ajax POST request and need to process the body data as a ByteBuffer and respond with an Array[Byte] using Http4s (0.23.7). This is as far I have been able to put things together, although it's not working yet:

// http4s/fs2/cats imports... +
import java.nio.ByteBuffer
import java.nio.channels.Channels

HttpRoutes.of[IO] {
  case ajaxRequest@POST -> Root / "ajax" / path =>
    val stream: fs2.Stream[IO, IO[Array[Byte]]] =
      ajaxRequest.body
        // 1. Convert body to InputStream (to enable converting to ByteBuffer)
        .through(fs2.io.toInputStream)
        .map { inputStream =>
          // 2. Create and populate ByteBuffer
          val byteBuffer = ByteBuffer.allocate(inputStream.available)
          Channels.newChannel(inputStream).read(byteBuffer)

          // 3. Process byteBuffer with library code
          val futResult: Future[Array[Byte]] = getRpcResult(path, byteBuffer)

          // 4. Convert Future to IO (?)
          val ioResult: IO[Array[Byte]] = IO.fromFuture(IO(futResult))
          ioResult
        }

    // 5. Convert stream to Response - how?
    Ok(stream)
      // 6. Set octet-stream content type
      .map(_.withContentType(
        `Content-Type`(MediaType.application.`octet-stream`, Charset.`UTF-8`)
      ).putHeaders(`Cache-Control`(NonEmptyList.of(`no-cache`()))))
}

Some questions:

For reference, here's how I do it in Play and Akka-Http:

// Play
def ajax(path: String): Action[RawBuffer] = {
  Action.async(parse.raw) { implicit ajaxRequest =>
    val byteBuffer = ajaxRequest.body.asBytes(parse.UNLIMITED).get.asByteBuffer
    getRpcResult(path, byteBuffer).map(Ok(_))
  }
}

// Akka-Http
path("ajax" / Remaining)(path =>
  post {
    extractRequest { req =>
      req.entity match {
        case HttpEntity.Strict(_, byteString) =>
          complete(getRpcResult(path, byteString.asByteBuffer))
      }
    }
  }
)

Thanks in advance for any advice!


Solution

  • The following code is written on top of my head and following the docs, since I can't test it right now.
    (thus it may have some typos / errors; if you find one please feel free to edit the answer and thanks!)

    HttpRoutes.of[IO] {
      case ajaxRequest @ POST -> Root / "ajax" / path =>
        val response =
          ajaxRequest.as[Array[Byte]].flatMap { byteArray =>
            IO.fromFuture(IO(
              getRpcResult(path, ByteBuffer.wrap(byteArray))
            ))
          }
    
        Ok(response) 
    }
    

    Let's expand on a couple of things.

    1. http4s supports reading and writing Array[Byte] out of the box, so no need to do funny things.
    2. Yes, you need to convert a Future to IO; and the way you did was wrong because the Future was already out of control. Also, if you would know what IO is, what problems does it solve and why http4s doesn't use Future; you wouldn't even be asking this question.
    3. You should hide that Future in a proper interface. This is more of general design advice more than a http4s specific one. That would not only make the code prettier but would make it easier to replace the legacy modules later or use a fake implementation in tests.

    Anyways, for looking at your code and the questions you ask it is clear that you are not familiar with the typelevel stack, nor with the "Programs as Values" paradigm.

    Thus, first of all, I would encourage you to join the Typelevel Discord server since it may be easier to help you through that platform than on StackOverflow.
    Second, I would suggest you ask yourself why are you using that stack? Is it for self-learning? Then great, but, please, follow an appropriate learning path, instead of just trying random code (after all, this is a completely new programming paradigm); it is for work? Ok but, then again, ask for support since you will be hitting one wall to another without proper guidance and you would end up hating this wonderful ecosystem; it is for another reason? Then again, think about what is the main driver and follow an appropriate plan.


    Additionally, you may want to see if something like fs2-grpc could be useful so you don't need to buffer all the request / response in memory, but rather stream everything in both ways.