API Platform Service Callouts using a Groovy Policy

Purpose

The purpose of this post is to demonstrate through a practical example how a Developer might enrich an API payload using a Service Callout from within a Groovy Policy.

 

Background

Since the release of Oracle API Platform v17.2.5, there has been a standard Policy entitled Service Callout 2.0 (version 1.0 having been deprecated at that release). That Policy facilitates the asynchronous invocation of an arbitrary endpoint on what is effectively a fire-and-forget basis. Using that Policy it is possible to invoke an external service with either of the GET, DELETE, POST or PUT HTTP methods. However, even though it is possible to construct a payload (for PUT or POST) it is not possible to manage any response payload from the external service. This post will demonstrate how the payload can be retrieved and used subsequently in the Request Pipeline. Note: Service Callouts are only available in the Request pipeline

Scenario

This scenario is contrived solely for the purposes of this discussion.

Let’s say that an API is defined in Oracle API Platform to which the consumer POSTs the following JSON payload:-

{

“BUILDING”: “100”,

“STREET”: “Oracle Parkway”,

“CITY”: “Redwood Shores”,

“STATE”: “CA”

}

The problem is that the Service Request requires a payload that also contains a ZIP code. Fortunately, we have a convenient RESTful endpoint (somewhere) that is capable of looking up a ZIP code based on the minimal values available. What we really want to POST to the defined Service Endpoint is this:-

{

“BUILDING”: “100”,

“STREET”: “Oracle Parkway”,

“CITY”: “Redwood Shores”,

“STATE”: “CA”,

“ZIP”: “94065”

}

Design considerations

It should never be any concern of an API consumer how a particular API is implemented. Of course, the consumer does need to know about how a payload (where relevant) should be constructed, what security is in place and so forth.

In reality, consumers may well be concerned about performance. A Gateway Node can handle 100s of concurrent APIs. But there are a finite number of Java threads available to process these APIs. Thus, it is possible that running large numbers of poorly performing (slow) concurrent Service Callouts synchronously, could cause exhaustion of the Weblogic thread pool with implications that are obvious. In order to avoid this potentiality, Service Callouts are executed asynchronously thereby releasing these threads until such time as the endpoint responds. It will come as no surprise that this is achieved by a typical callback mechanism.

And so, apart from duration of the external service, the consumer should merely abide by the API’s documentation which is typically described in Swagger, API Blueprint or RESTful API Modelling Language (RAML)

Let’s create an API

Phase 1

AK_Phase1

This will be familiar to API Platform Developers. We have a Request Pipeline with an API Endpoint named SCBLOG and we see the Service Request. For the purposes of this blog post, the Service Request is simply a passthrough – i.e. whatever payload is POSTed to it is returned without modification.

Let’s add a Groovy Policy immediately after the API Request. We’ll call it “GET ZIP Groovy”. The Groovy code will look like this:-

ExternalServiceCallout callout = context.newCallout()

ExternalServiceCallout.Callback callback = new ExternalServiceCallout.Callback() {

  boolean onCompleted(ServiceResponse response) throws PolicyProcessingException {
    context.getServiceRequest().setBody(response.getBody())
    return true
  }

  boolean onThrowable(Throwable throwable) throws PolicyProcessingException {
    return true
  }
}

callout.setHeader("Content-Type", context.getApiRequest().getHeader("Content-Type"))
callout.setBody(context.getApiRequest().getBody())
callout.setMethod("POST")
callout.setRequestURL("http://localhost:2222/getzip")
callout.sendAsync(callback)

 

So what’s going on here?

First of all, we need an object that will manage the service callout. Then we need a callback object that implements the onCompleted and onThrowable methods (more on this in a moment). Then we need to set up the callout appropriately.

Always set the Content-Type. Do not set Content-Length. [ This is done for you. In fact, if you do set it, you’ll get a runtime failure ]. Set the HTTP Method. Set the Request URL (hard-coded here for demo’ purposes only) and ultimately we call sendAsync. This where the asynchronous invocation of our external service is conceptually initiated.

Note that I said that it’s “conceptually” initiated at this point. The thread that is executing the Request pipeline is only suspended (released to the Weblogic thread pool) once this Policy has completed. It is therefore generally advisable that the call to sendAsync should be the last line in the code.

Once the external service has completed normally, the onCompleted method of our callback class will be executed.

Currently (v18.2.1), although the interface definition requires implementation of onThrowable it is not used. In the future, when this is fully implemented, I recommend that you construct a PolicyProcessingException and throw that (at which point all pipeline processing will halt and the API consumer will observe the HTTP error declared in the PolicyProcessingException constructor). At this time if, for example, the external service is unavailable, then a traditional HTTP 503 error will be observed by the API consumer and cannot be handled as part of the callback mechanism.

Phase 2

Deploy and test the API that now looks like this:-

Testing

For this aspect, we will be using Oracle API Platform v18.2.1, Postman and an external endpoint emulator implemented in Python. The API that we have built is assumed to be deployed to a Gateway Node accessible as:-

http://\<GATEWAYHOST\>:\<GATEWAYPORT\>/SCBLOG

You will note that the passthrough Service Request is available on localhost as is the getzip endpoint- i.e. the same host that’s running the Gateway Node that we’re testing against. Wherever you’re going to run this Service Request, you’ll need Python 3 installed along with the Flask module. You then just need the following Python code:-

from flask import (Flask, request, Response, abort)
from functools import wraps
import json

app = Flask(__name__)

def returns_json(j):
  @wraps(j)
  def decorated_function(*args, **kwargs):
    r = j(*args, **kwargs)
    return Response(r, content_type='application/json; charset=utf-8')
  return decorated_function


@app.route('/passthrough', methods=['POST'])
@returns_json
def passthrough():
  return json.dumps(request.get_json())


@app.route('/getzip', methods=['POST'])
@returns_json
def getzip():
  j = request.get_json()
  if 'ZIP' not in j:
    j['ZIP'] = "94065"
  return json.dumps(j)


if __name__ == "__main__":
  app.run(port=2222)



With this code executing in an appropriate location, we can now move to Postman where we’ll POST to our API and, if you’ve followed the steps outlined about, you’ll see the following output:-

AK_Final

Of course, there’s no real ZIP-code lookup happening in the emulator but I’m sure you see the point.

 

Add Your Comment