Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

HTTP Proxy Using Play Framework and WS API

5.00/5 (2 votes)
28 Oct 2015CPOL3 min read 16K   174  
Creating a HTTP proxy using Play framework and WS API

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:

Image 1

And here is what has been achieved:

Image 2

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.

  1. Request is received by the application.
  2. In route function either transparent call or interception might be performed.
  3. 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.
  4. 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:

C++
class Application extends Controller { 
  def route = Action.async { implicit request =>

    // TODO: do routing depends on either headers or body.
    // at this point request contains body

    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).

C++
def transparentCall = Action.async { implicit request =>

  // Modifying body, e.g. to substitute caller url if server has WCF "To-filter" endpoint check

  val thisUrl = s"http://{request.host}"
  val targetUrl = ...
  val strBody = request.body.asXml match {
    case Some(xml) => xml.toString().replace(thisUrl, targetUrl)
    case None => ""
  }

  // Transforming Request -> WSRequest, 
  // perform call and transforming back WSResponse -> Result

  WS.url(target)
    .withMethod(request.method)
    .withHeaders(request.headers.toSimpleMap.toList: _*)
    .withBody[String](strBody) // Content-Length header will be updated automatically
    .execute()
    .map {
      response => {
        val body = 
        	response.body.replace(targetUrl, thisUrl) // replace the caller url back
        val headers = 
        	response.allHeaders map { // fix the "Content-Lenght" header manually
          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

  • First post

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)