Search code examples
scalaplayframework

Play framework forward request/response with big body


I am using the play framework to forward request to some internal services. One of these services return pretty big payloads (5MB json data). When I call the service directly it takes around 5sec to process and return the request (there is happening some processing). When I use play to forward the request and return the response, it takes 3 times longer. I also got some java.lang.OutOfMemoryError: Java heap space at play.shaded.ahc.org.asynchttpclient.netty.NettyResponse.getResponseBodyAsByteBuffer errors which is suspect come from the parsing the json body which is unnecessary. Is there any way to forward the request and return the response without parsing it?

Currently my code looks like this:

def process = Authorized().async { request =>
  request.body.asJson.map { body =>
    ws.url("internal-service/process")
      .addHttpHeaders("Accept-Encoding" -> "gzip, deflate")
      .post(body).map { res =>
      if (res.status >= 200 && res.status < 300) {
        Ok(res.json)
      } else {
        throw SimpleServiceException(s"Failed to process datasets. Reason ${res.status}: ${res.body}")
      }
    }
  }.getOrElse(Future.successful(BadRequest))
}

Solution

  • First, let's understand what's causing the issue in your code.

    When you call res.json, you're telling Play to consume entirely the response, parse it as JSON, keep it in memory to then be able to send it back.

    This is not what you want to do.

    Instead, you can parse the response as a Source (Akka Stream) so that it's consumed lazily/progressively.

    Do something like this:

    ws
      .url(...)
      .withMethod("POST")
      .stream()
      .map { response =>
        Ok.chunked(response.bodyAsSource)
      }
    

    The two important things are:

    • using .withMethod(...).stream() rather than .post() to get a streaming response
    • using Ok.chunked(...) to stream back to your client

    See this useful documentation for more details. It even contains a code sample for what you want to do:

    Another common destination for response bodies is to stream them back from a controller’s Action:

    def downloadFile = Action.async {   // Make the request  
      ws.url(url).withMethod("GET").stream().map { response =>
        // Check that the response was successful
        if (response.status == 200) {
          // Get the content type
          val contentType = response.headers
            .get("Content-Type")
            .flatMap(_.headOption)
            .getOrElse("application/octet-stream")
    
          // If there's a content length, send that, otherwise return the body chunked
          response.headers.get("Content-Length") match {
            case Some(Seq(length)) =>
              Ok.sendEntity(HttpEntity.Streamed(response.bodyAsSource, Some(length.toLong), Some(contentType)))
            case _ =>
    
              Ok.chunked(response.bodyAsSource).as(contentType)
          }
        } else {
          BadGateway
        }  
      }
    }
    

    As you may have noticed, before calling stream() we need to set the HTTP method to use by calling withMethod on the request.