Jacob Wang
Jacob Wang

Reputation: 4794

Proxying requests in Play Framework

How do I do a full proxy with Play Framework?

I want to keep the headers and body intact for both the request and the response. Basically, a transparent proxying layer to both the client and server.

Note: I've got something working. Will post it when SO allows me to.

Upvotes: 2

Views: 1123

Answers (1)

Jacob Wang
Jacob Wang

Reputation: 4794

This is what I end up with.

With my (not comprehensive) testing this works for all methods with various body types.

Note my use of _.head. I haven't dug into why headers have the type Map[String, Seq[String]]. I might be dropping duplicate header contents (e.g. having more than on Content-Type in the header). Perhaps joining the Seq[String] with ; is the better way.

import play.api.libs.ws._
import play.api.libs.iteratee.Enumerator
import play.api.mvc._

def proxy(proxyUrl: String) = Action.async(BodyParsers.parse.raw) { request =>
  // filter out the Host and potentially any other header we don't want
  val headers: Seq[(String, String)] = request.headers.toSimpleMap.toSeq.filter {
    case (headerStr, _) if headerStr != "Host" => true
    case _ => false
  }
  val wsRequestBase: WSRequestHolder = WS.url(s"http://localhost:9000/$proxyUrl") // set the proxy path
    .withMethod(request.method) // set our HTTP method
    .withHeaders(headers : _*) // Set our headers, function takes var args so we need to "explode" the Seq to var args
    .withQueryString(request.queryString.mapValues(_.head).toSeq: _*) // similarly for query strings
  // depending on whether we have a body, append it in our request
  val wsRequest: WSRequestHolder = request.body.asBytes() match {
    case Some(bytes) => wsRequestBase.withBody(bytes)
    case None => wsRequestBase
  }
  wsRequest
    .stream() // send the request. We want to process the response body as a stream
    .map { case (responseHeader: WSResponseHeaders, bodyStream: Enumerator[Array[Byte]]) => // we want to read the raw bytes for the body
      // Content stream is an enumerator. It's a 'stream' that generates Array[Byte]
      new Result(
        new ResponseHeader(responseHeader.status),
        bodyStream
      )
      .withHeaders(responseHeader.headers.mapValues(_.head).toSeq: _*)
    }
}

routes file entry will look something like this:

GET     /proxy/*proxyUrl                   @controllers.Application.proxy(proxyUrl: String)

You will need to other lines to support other methods (e.g. POST)

Feel free to suggest edits.

Upvotes: 2

Related Questions