Introduction
The purpose of this tip and code attached is to explain a few Play Framework basics and its WS API. It is not considered as something very useful by itself but rather as a starting point for similar tasks.
Background
This code is taken from real life project where it was needed to isolate and "stub" a bunch of WCF Web services without changing codebase of either clients or web service. The only thing that was changed at clients' side was the address of the target web service. In addition, term "Stub
" here means to intercept HTTP call to target service and return some meaningful enough result, e.g., for testing purposes.
It is expected that reader understands the basics of HTTP protocol and HTTP message format.
So here is how things were originally. Pretty simple:
And here is what has been achieved:
Later on this tip, I will focus on the key points of the proxy.
How Proxy Works
The proxy is as simple as play framework application. But requests are considered as plain HTTP requests without any distinguishing on method, parameters, etc. Here Request
and Result
are play framework objects, and WSRequest
and WSResponse
are WS API objects.
- Request is received by the application.
- In route function either transparent call or interception might be performed.
- If it is transparent call, the following action is performed:
Request
is transformed to WSRequest
. - WS call to the target web service endpoint is performed.
WSResult
obtained is transformed to play framework Result
. Result
object is returned to the client.
- If interception is performed, the saved HTTP response template is returned as a
Result
.
Using the Code
The code is built on using Play Framework 2.4.x on Scala. The solution is attached, and key features are described further.
Routes File
Contains only one single "fake" record just because it is not possible to get rid of it in play framework.
Global Object
Global
object is introduced and it overrides onRequestReceived()
function as shown below. As soon as there is no call to base class, the standard play framework routing functionality is not invoked.
object Global extends GlobalSettings {
override def onRequestReceived(request : play.api.mvc.RequestHeader) :
(play.api.mvc.RequestHeader, play.api.mvc.Handler) = {
// at this point request dot contain body
if (Play.configuration.getBoolean("transparent").getOrElse(true))
(request, new Application().transparentCall)
else
(request, new Application().route)
}
...
}
The "transparent
" boolean variable is introduced in conf file to make it possible to work as a transparent proxy only.
Application Controller
In our case, it contains two important functions:
route()
which is responsible to analyze request and call either an interceptor function or the transparentCall()
function transparentCall()
which contains all the logic to make WS API call and return the result of this call
route()
action function looks as follows:
class Application extends Controller {
def route = Action.async { implicit request =>
if (request.body.contains("...whatever...") {
doInterceptCallAndReturnStubResult(request)
} else {
transparentCall(request)
}
}
}
transparentCall()
action function looks like shown below. As an example, there is a small transformation of body that has been performed (caller address is substituted in body to pass, e.g., caller address check on server).
def transparentCall = Action.async { implicit request =>
val thisUrl = s"http://{request.host}"
val targetUrl = ...
val strBody = request.body.asXml match {
case Some(xml) => xml.toString().replace(thisUrl, targetUrl)
case None => ""
}
WS.url(target)
.withMethod(request.method)
.withHeaders(request.headers.toSimpleMap.toList: _*)
.withBody[String](strBody) .execute()
.map {
response => {
val body =
response.body.replace(targetUrl, thisUrl) val headers =
response.allHeaders map { h => (h._1, if (h._1 == "Content-Length")
body.length.toString else h._2.head)
}
Result(ResponseHeader(response.status, headers), Enumerator(body.getBytes))
}
}
}
Points of Interest
Play framework Request
/Result
and WSRequest
/WSResponse
are different types despite both being wrappers on HTTP and there is no standard mapping between them.
To stub the result, the easiest way is to log the actual call to target system and save response as a template for further use. It is especially useful for SOAP calls.
As a bonus, the attached solution contains the package with functions for writing/loading HTTP request and responses to web services to simplify stub creation.
History