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.
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))
})
}
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
}
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:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With