Manipulating scoped properties in Prototyped APIs in WSO2 API Manager

Raj Rajaratnam
Aug 2, 2018 · 8 min read

Note: In this article, I’m explaining a specific usecase where you want to extend the extend the API prototyping capabilities of WSO2 API Manager, but the same concept applies to any other usecases.


WSO2 API Manager allows you to deploy API prototypes for the purpose of early promotion and testing. You can deploy a new API or a new version of an existing API as a prototype. It gives subscribers an early implementation of the API that they can try out without a subscription or monetization, and provide feedback to improve. After a period of time, publishers can make changes the users request and publish the API.

When we implement a prototyped API with advanced usecases, we may find some gaps in the out of the box capabilities provided by prototyped APIs. To name a few;

  • We can’t read incoming HTTP headers in prototyped APIs
  • We can’t set outgoing HTTP headers in prototyped APIs

This blog post explains how we can extend API Manager to manipulate scoped properties in prototyped APIs. Doing so will allow us to do some advanced stuffs in prototyped APIs including but not limited to read incoming or set outgoing HTTP headers.


Internals

Before diving into the details, let’s know some internals which will help us to understand the solution;

  • Prototyped API’s core capability is implemented using script mediator
  • Only the properties set in the default context (a.k.a scope) can be manipulated using the script mediator. Properties set in other contexts (transport and axis2) can be manipulated using a sequence mediator
  • Incoming HTTP headers will be available as transport scoped properties in the mediation engine. We can access them using $trp:headerName
  • If you create a property with transport scope, it will be sent as an outgoing HTTP header.
  • There are some special properties in axis2 scope like HTTP_SC that can be used to set the HTTP Status Code of the response.
  • API Manager (Publisher UI, to be accurate) doesn’t support attaching a mediation sequence to a prototyped API.
  • Prototyped API’s definition (what should be included in api.xml deployed to Gateway) is determined by APIM_HOME/repository/resources/api_templates/prototype_template.xml
  • When we deploy a prototyped API from Publisher UI, it creates the API definition based on prototype_template.xml together with the information provided in the Publisher UI and deploy it to Gateway. This process happens everytime we redeploy the API. Hence, manual changes we do to the API definition in the Gateway will be overwritten when we redeploy the API next time.
  • Prototype_template.xml can be updated to include additional mediators we want in the prototyped APIs.

Let’s come up with a use-case for a prototyped API.


Utility API

The Utility API is a prototyped API, deployed for our customers who want to check the connectivity with our API Management platform.

Customers send the following request with Content-Type: application/json header;

{ “request”: “hello” }

And they receive the following response with 200 HTTP Status Code;

{ “response”: “hello” }

If they don’t send Content-Type: application/json header, they receive the following response with 400 HTTP Status Code;

{ “response”: “bad request” }

Let’s create the Utility API with POST /hello resource and embedded above logic in the inline script, as shown below.

var reqPayload = mc.getPayloadJSON();var resPayload = {};
if (mc.getProperty('Content-Type') == 'application/json') {
resPayload = {"response": reqPayload.request};
mc.setProperty("HTTP_SC", "200");
} else {
resPayload = {"response": "bad request"};
mc.setProperty("HTTP_SC", "400");
}
mc.setPayloadJSON(resPayload);
mc.setProperty('CONTENT_TYPE', 'application/json');

After deploying this as a prototyped API, let’s have look at the API artifact in the Gateway, at APIM_HOME/repository/deployment/server/synapse-configs/default/api/admin--Utility_v1.0.xml. Note that the API name is prefixed with the provider name who created the API.

<?xml version="1.0" encoding="UTF-8"?>
<api xmlns="http://ws.apache.org/ns/synapse" name="admin--Utility" context="/utility/1.0" version="1.0" version-type="context">
<resource methods="POST" url-mapping="/hello" faultSequence="fault">
<inSequence>
<script language="js">
var reqPayload = mc.getPayloadJSON();

var resPayload = {};
if (mc.getProperty('Content-Type') == 'application/json') {
resPayload = {"response": reqPayload.request};
mc.setProperty("HTTP_SC", "200");
} else {
resPayload = {"response": "bad request"};
mc.setProperty("HTTP_SC", "400");
}
mc.setPayloadJSON(resPayload);
mc.setProperty('CONTENT_TYPE', 'application/json');
</script>
<filter source="boolean(get-property('CONTENT_TYPE'))" regex="false">
<then>
<property name="messageType" value="application/xml" scope="axis2" />
</then>
<else>
<property name="messageType" expression="get-property('CONTENT_TYPE')" scope="axis2" />
</else>
</filter>
<respond />
</inSequence>
<outSequence>
<send />
</outSequence>
</resource>
<handlers>
<handler class="org.wso2.carbon.apimgt.gateway.handlers.security.CORSRequestHandler">
<property name="apiImplementationType" value="INLINE" />
</handler>
</handlers>
</api>

Pretty straightforward API definition. A script mediator followed by couple of filter and property mediators. As I mentioned earlier, the content of API definition is generated based on prototype_template.xml. If we want to include an additional mediator in the flow, we would update prototype_template.xml.

We are not done yet. Even if we now send the request as{ “request”: “hello” } with Content-Type: application/json, we will get { “response”: “bad request” } as response. Reason is that the transport or axix2 scope properties can’t be manipulated (get or set) in script mediator.

Let’s see how we can update prototype_template.xml to include two sequence mediators in prototyped API definition, one before script mediator and another one after the script mediator. We are doing this because we can manipulate transport or axis2 scope properties in a sequence mediator.


Updating prototype_template.xml

Let’s open APIM_HOME/repository/resources/api_templates/prototype_template.xml in Publisher node, search for “script” and find a script mediator definition. There will be only one script mediator by default. Let’s add two sequence mediators before and after the script mediator, as shown below. API details like name, version, context, etc are available inside the template, hence we are using them (!apiName and !apiVersion) to name our sequences. In other words, we are engaging sequences in API level, not globally.

#set ($beforeScriptSeqKey = "--beforeScript_v")
#set ($beforeScriptSeqKey = "$!apiName$beforeScriptSeqKey$!apiVersion")
<sequence key="$beforeScriptSeqKey"/>

#if(!$resource.getMediationScript().equalsIgnoreCase("null"))
<script language="js">
<![CDATA[
$resource.getMediationScript()
]]>
</script>
#set ($afterScriptSeqKey = "--afterScript_v")
#set ($afterScriptSeqKey = "$!apiName$afterScriptSeqKey$!apiVersion")
<sequence key="$afterScriptSeqKey"/>

I reiterate that we need to do this change in Publisher node, not Gateway.

Let’s redeploy the API in Publisher UI and have look at the API artifact in the Gateway, at APIM_HOME/repository/deployment/server/synapse-configs/default/api/admin--Utility_v1.0.xml

<?xml version="1.0" encoding="UTF-8"?>
<api xmlns="http://ws.apache.org/ns/synapse" name="admin--Utility" context="/utility/1.0" version="1.0" version-type="context">
<resource methods="POST" url-mapping="/hello" faultSequence="fault">
<inSequence>
<sequence key="admin--Utility--beforeScript_v1.0"/>
<script language="js">
var reqPayload = mc.getPayloadJSON();

var resPayload = {};
if (mc.getProperty('Content-Type') == 'application/json') {
resPayload = {"response": reqPayload.request};
mc.setProperty("HTTP_SC", "200");
} else {
resPayload = {"response": "bad request"};
mc.setProperty("HTTP_SC", "400");
}
mc.setPayloadJSON(resPayload);
mc.setProperty('CONTENT_TYPE', 'application/json');
</script>
<sequence key="admin--Utility--afterScript_v1.0"/>
<filter source="boolean(get-property('CONTENT_TYPE'))" regex="false">
<then>
<property name="messageType" value="application/xml" scope="axis2" />
</then>
<else>
<property name="messageType" expression="get-property('CONTENT_TYPE')" scope="axis2" />
</else>
</filter>
<respond />
</inSequence>
<outSequence>
<send />
</outSequence>
</resource>
<handlers>
<handler class="org.wso2.carbon.apimgt.gateway.handlers.security.CORSRequestHandler">
<property name="apiImplementationType" value="INLINE" />
</handler>
</handlers>
</api>

We can see a sequence mediator <sequence key=”admin--Utility--beforeScript_v1.0"/> is added before script mediator and another sequence mediator <sequence key=”admin--Utility--afterScript_v1.0"/> is added after the script mediator automatically.

Now we have to create and deploy these two sequences and deploy in Gateway manually. We can use puppet to automate the deployment.


Creating sequences

Let’s create admin--Utility--beforeScript_v1.0.xml with the following content and deploy it to Gateway, at APIM_HOME/repository/deployment/server/synapse-configs/default/sequences

<sequence xmlns="http://ws.apache.org/ns/synapse" name="admin--Utility--beforeScript_v1.0">
<property name="Content-Type" expression="$trp:Content-Type"/>
<log level="custom">
<property name="where" expression="fn:concat('admin-Utility-beforeScript_v1.0: ', $ctx:Content-Type)"/>
</log>
</sequence>

What we are doing here is that we are reading the value of Content-Type property in transport scope and set it to Content-Type property in default scope so that it can be manipulated in script mediator.

Let’s create admin--Utility--afterScript_v1.0.xml with the following content and deploy it to Gateway, at APIM_HOME/repository/deployment/server/synapse-configs/default/sequences

<sequence xmlns="http://ws.apache.org/ns/synapse" name="admin--Utility--afterScript_v1.0">
<property name="HTTP_SC" expression="$ctx:HTTP_SC" scope="axis2"/>
<log level="custom">
<property name="where" expression="fn:concat('admin-Utility-afterScript_v1.0: ', $axis2:HTTP_SC)"/>
</log>
</sequence>

What we are doing here is that we are reading the value of HTTP_SC property in default scope and set it to HTTP_SC property in axis2 scope so that it will be sent as the HTTP Status Code to the client.


Testing

Send the request { “request”: “hello” }with Content-Type: application/json and you will get the response { “response”: “hello” } with 200 HTTP Status Code

Rajs-MacBook-Pro:sequences raj$ curl -v -X POST --header "Content-Type: application/json" --header "Accept: application/json" -d '{"request":"hello"}' "http://127.0.0.1:8280/utility/1.0/hello"

* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8280 (#0)
> POST /utility/1.0/hello HTTP/1.1
> Host: 127.0.0.1:8280
> User-Agent: curl/7.54.0
> Content-Type: application/json
> Accept: application/json
> Content-Length: 19

>
* upload completely sent off: 19 out of 19 bytes
< HTTP/1.1 200 OK
< Accept: application/json
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Methods: POST
< Host: 127.0.0.1:8280
< Access-Control-Allow-Headers: authorization,Access-Control-Allow-Origin,Content-Type
< Content-Type: application/json; charset=UTF-8
< Date: Thu, 02 Aug 2018 18:12:31 GMT
< Transfer-Encoding: chunked
<
* Connection #0 to host 127.0.0.1 left intact
{"response":"hello"}

Send the request { “request”: “hello” }with Content-Type: text/plain and you will get the response { “response”: “bad request” } with 400 HTTP Status Code

Rajs-MacBook-Pro:sequences raj$ curl -v -X POST --header "Content-Type: text/plain" --header "Accept: text/plain" -d '{"request":"hello"}' "http://127.0.0.1:8280/utility/1.0/hello"

* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8280 (#0)
> POST /utility/1.0/hello HTTP/1.1
> Host: 127.0.0.1:8280
> User-Agent: curl/7.54.0
> Content-Type: text/plain
> Accept: text/plain
> Content-Length: 19

>
* upload completely sent off: 19 out of 19 bytes
< HTTP/1.1 400 Bad Request
< Accept: text/plain
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Methods: POST
< Host: 127.0.0.1:8280
< Access-Control-Allow-Headers: authorization,Access-Control-Allow-Origin,Content-Type
< Content-Type: application/json; charset=UTF-8
< Date: Thu, 02 Aug 2018 18:17:15 GMT
< Transfer-Encoding: chunked
< Connection: Close
<
* Closing connection 0
{"response":"bad request"}

Global Sequence

The solution we designed above engages two additional sequences in API level. In other words, we will need to create two new sequences for every prototyped API. Instead (or in addition to this), if we want to have a common sequence for all our prototyped APIs, we can do that too.

Let’s update the prototype_template.xml as shown below.

<sequence key="beforeScript"/>
#if(!$resource.getMediationScript().equalsIgnoreCase("null"))
<script language="js">
<![CDATA[
$resource.getMediationScript()
]]>
</script>
<sequence key="afterScript"/>

What we are doing here is that we are just removing API name and version from the sequence name. We now have to create two sequences beforeScript.xml and afterScript.xml and deploy to Gateway. When we redeploy our API from Publisher UI, our API definition will be updated, as shown below.

<?xml version="1.0" encoding="UTF-8"?>
<api xmlns="http://ws.apache.org/ns/synapse" name="admin--Utility" context="/utility/1.0" version="1.0" version-type="context">
<resource methods="POST" url-mapping="/hello" faultSequence="fault">
<inSequence>
<sequence key="beforeScript" />
<script language="js">var reqPayload = mc.getPayloadJSON();var resPayload = {};if (mc.getProperty('Content-Type') == 'application/json') { resPayload = {"response": reqPayload.request}; mc.setProperty("HTTP_SC", "200");} else { resPayload = {"response": "bad request"}; mc.setProperty("HTTP_SC", "400");}mc.setPayloadJSON(resPayload);mc.setProperty('CONTENT_TYPE', 'application/json');</script>
<sequence key="afterScript" />
<filter source="boolean(get-property('CONTENT_TYPE'))" regex="false">
<then>
<property name="messageType" value="application/xml" scope="axis2" />
</then>
<else>
<property name="messageType" expression="get-property('CONTENT_TYPE')" scope="axis2" />
</else>
</filter>
<respond />
</inSequence>
<outSequence>
<send />
</outSequence>
</resource>
<handlers>
<handler class="org.wso2.carbon.apimgt.gateway.handlers.security.CORSRequestHandler">
<property name="apiImplementationType" value="INLINE" />
</handler>
</handlers>
</api>

Since these sequences doesn’t have any API name in it, they will be executed for all the prototyped APIs.


Of course, if we want we can have both global and API level sequences. Let’s update the prototype_template.xml as shown below.

#set ($beforeScriptSeqKey = "--beforeScript_v")
#set ($beforeScriptSeqKey = "$!apiName$beforeScriptSeqKey$!apiVersion")
<sequence key="beforeScript"/>
<sequence key="$beforeScriptSeqKey"/>

#if(!$resource.getMediationScript().equalsIgnoreCase("null"))
<script language="js">
<![CDATA[
$resource.getMediationScript()
]]>
</script>
#set ($afterScriptSeqKey = "--afterScript_v")
#set ($afterScriptSeqKey = "$!apiName$afterScriptSeqKey$!apiVersion")
<sequence key="afterScript"/>
<sequence key="$afterScriptSeqKey"/>

Thank you for scrolling till the end 🙏

Raj Rajaratnam

Written by

My writings are not official docs

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade