Play: Binary Webservice answer

I need to call a web service that gives me the contents of a binary file. I just want to return the same content to the calling 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 binaries will be corrupted. What am I doing wrong here? (Maybe it's f.body that encodes the binary result from the webservice to String? But how can I get the raw data?)

I know this is not a good way for large files - I read in the Play Docs on HTTP streaming responses , but it seems to me that this is difficult for me to be new to the Play platform.

+6
source share
3 answers

You can get raw data with f.ahcResponse.gerResponseBodyAsBytes . But I think this will load the whole answer into memory, which is inefficient.

You can use the streaming features that play! pretty easy looks 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)) }) } 
+8
source

If you want to transfer 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 } 
+3
source

Based on the answer by Yann Simon, here is a simple CORS proxy implementation that allows you to transfer downloaded remote files and transfer them to the client. It does not load the entire file into 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 allows you to create an Iteratee / Enumerator pair, so whenever you add bytes to Iteratee, these bytes will be enumerated by an enumerator.

This was the missing item because:

  • You need Iteratee to use the WS answer.
  • You need an Enumerator to create a response in the game.
+1
source

Source: https://habr.com/ru/post/928204/


All Articles