Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Play: Binary webservice response

I have to call a webservice that gives me the content of a binary file. I just want to give the same content back to the caller of my controller:

val blobPromise = WS.url("http://url/to/webservice/file.txt").get()
Async {
  blobPromise.map(f => Ok(f.body))
}

This works for text files, but binary files will get corrupted. What I am doing wrong here? (Maybe it's the f.body that encodes the binary result from the webservice to a String? But how can I get the raw data?)

I know, it's not a good way for big files - I have read in the Play docs about Streaming HTTP responses, but it seems to complicated for me as a beginner with the Play framework.

like image 863
Sonson123 Avatar asked Oct 20 '12 20:10

Sonson123


3 Answers

You can get the raw data using f.ahcResponse.gerResponseBodyAsBytes. But I think, this will load the entire response into memory, which is inefficient.

You can use the streaming functionality that Play! provides quite easily like this:

Async {
  WS.url("http://url/to/webservice/file.txt").get().map(response => {
    val asStream: InputStream = response.ahcResponse.getResponseBodyAsStream
    Ok.stream(Enumerator.fromStream(asStream))
  })
}
like image 66
Ratan Sebastian Avatar answered Oct 22 '22 15:10

Ratan Sebastian


If you want to stream the content:

def streamFromWS = Action.async { request =>
  import play.api.libs.iteratee.Concurrent.joined

  val resultPromise = Promise[SimpleResult]

  val consumer = { rs: ResponseHeaders =>
    val (wsConsumer, stream) = joined[Array[Byte]]
    val contentLength = rs.headers.get("Content-Length").map(_.head).get
    val contentType = rs.headers.get("Content-Type").map(_.head).getOrElse("binary/octet-stream")
    resultPromise.success(
      SimpleResult(
        header = ResponseHeader(
          status = OK,
          headers = Map(
            CONTENT_LENGTH -> contentLength,
            CONTENT_DISPOSITION -> s"""attachment; filename="file.txt"""",
            CONTENT_TYPE -> contentType
          )),
        body = stream
      ))
    wsConsumer
  }

  WS.url("http://url/to/webservice/file.txt").get(consumer).map(_.run)

  resultPromise.future
}
like image 42
Yann Simon Avatar answered Oct 22 '22 13:10

Yann Simon


Based on Yann Simon answer, here's a simple CORS proxy implementation that permits to stream downloaded remote files and stream them to the client. It does not load all the file in memory.

  import play.api.libs.iteratee._

  private def getAndForwardStream(requestHolder: WSRequestHolder)(computeHeaders: ResponseHeaders => ResponseHeader): Future[SimpleResult] = {
    val resultPromise = scala.concurrent.Promise[SimpleResult]
    requestHolder.get { wsResponseHeaders: ResponseHeaders =>
      val (wsResponseIteratee, wsResponseEnumerator) = Concurrent.joined[Array[Byte]]
      val result = SimpleResult(
        header = computeHeaders(wsResponseHeaders),
        body = wsResponseEnumerator
      )
      resultPromise.success(result)
      wsResponseIteratee
    }
    resultPromise.future
  }

  def corsProxy(url: URL) = Action.async { implicit request =>
    val requestHolder = WS.url(url.toString).withRequestTimeout(10000)
    getAndForwardStream(requestHolder) { wsResponseHeaders: ResponseHeaders =>
      // We use the WS response headers and transmit them unchanged to the client, except we add the CORS header...
      val originToAllow = request.headers.get("Origin").getOrElse("*")
      val headers = wsResponseHeaders.headers.mapValues(_.head) + ("Access-Control-Allow-Origin" -> originToAllow)
      ResponseHeader(
        status = wsResponseHeaders.status,
        headers = headers
      )
    }
  }

The important part here is the use of play.api.libs.iteratee.Concurrent.joined[Array[Byte]]. It permits to create an Iteratee/Enumerator pair so that whenever you add bytes to the Iteratee, these bytes will be enumerator by the enumerator.

This was the missing piece because:

  • You need an Iteratee to consume the WS response.
  • You need an Enumerator to produce the play framework response.
like image 1
Sebastien Lorber Avatar answered Oct 22 '22 14:10

Sebastien Lorber