Custom JavaScript: Examples

You can see examples of scripts to add to your custom JavaScript interceptors here.

Abort requests to old resources

This script can be used to block access to an old URL, but with a friendly message to developers still using that URL. You should follow the scripts below, adding the URL to the URL Pattern field.

This snippet indicates that the Gateway will not accept the request. It must be added to the request flow.

$call.decision.setAccept(false);

Add the following snippet, modifying the response to set a 404 status code and fixed body. It should be executed in the after-response step.

$call.response.setStatus(404);
$call.response.getBody().setString(
  '{"message": "This resource has been removed. Use /orders/status instead."}', "utf-8"
);

Add header from a token’s extraInfo

This script can be used to add a header to a request. The content of this header will be obtained from the extraInfo of one of the tokens used in the request.

In the example below, a header X-Customer-Id will be added, containing the client ID represented by the existing access token.

var access_token = $call.accessToken;
if (access_token) {
    $request.setHeader("X-Customer-Id", access_token.extraInfo.get('customerId'));
}

To test the script above, make the following request. This test assumes that there is an access token with the code mycustomer containing an extraInfo called customerId and value 123.

curl -H "access_token: mycustomer" http://10.0.0.5:8080/example

Among the headers received by the backend, there must be:

...
"headers": {
    ...
    "X-Customer-Id": "123"
    ...
}

See that in the script above we are assuming that there will always be an access token. If the script is executed in the request flow and the access token is required (as it usually is), then this is a sound assumption, since the gateway would block the access if the token was not informed.

If the token is optional, however, we must deal with the case of it not being informed. The example below adds a header indicating that this app is being tested in a sandbox environment, but the app might not be informed.

var app_token = $call.app;
if ( app_token && app_token.extraInfo.get('isSandbox') == "true" ) {
    $request.setHeader( "X-Is-Sandbox", "true" );
}

To test the script above, make the request below. This test assumes that there is an app with the code myapp, containing an extraInfo called isSandbox and value true.

curl -H "client_id: myapp" http://10.0.0.5:8080/example

Among the headers received by the backend, there must be:

...
"headers": {
    ...
    "X-Is-Sandbox": "true"
    ...
}

Add fixed header

This script can be used to add a header to the request. In the example below, we are inserting a Content-Type: application/json only if the request does not contain a Content-Type

if ( $call.request.getHeader( "Content-Type" ) == null )
    $call.request.setHeader( "Content-Type", "application/json" );

Add pagination query params when absent

In this example, we have a backend that always expects to receive pagination parameters in query-strings (e.g., /products?page=5&page_size=10). But we want requests without these parameters to be executed with sensible default values (page=0 and page_size=10, for instance).

Note that we do not want to add parameters if they already exist.

if ( ! $request.getQueryParam("page") )
    $request.setQueryParam("page", "0");

if ( ! $request.getQueryParam("page_size") )
    $request.setQueryParam("page_size", "20");

We also want to protect ourselves against calls that specify pagination parameters beyond the expected, so we must continue the script above with:

var page = parseInt($request.getQueryParam("page"));
if (page > 100 || page < 0)
    $request.setQueryParam("page", 0);

var pageSize = parseInt($request.getQueryParam("page_size"));
if (pageSize > 100 || pageSize < 1)
    $request.setQueryParam("page_size", 20);

Alter route according to a token’s extraInfo

In this example, the route to be followed differs depending on a flag in the extraInfo of an access token indicating whether it is a legacy or not. If so, it should be routed to http://legacy.mycompany.com); if not, it should be forwarded to http://api.mycompany.com).

Note that at least one route must be defined, otherwise the Gateway will consider the request as irregular and will return an HTTP 404 status code before the script is executed.

var access_token = $call.accessToken;
if (access_token && access_token.extraInfo.get('isLegacy') == "true") {
    var newDest = 'http://legacy.backend.com';

    $call.setDestinationUri(new Packages.java.net.URI(newDest));
    $call.request.setHeader('Host', $call.destinationUri.getHost());
}

As a test, you can create an app with the extraInfo isLegacy = true and make a request. After, modify the flag (or create another app with isLegacy = false) and make another request.

curl -H 'client_id: mynewapp' http://10.0.0.5:8080/example/api

curl -H 'client_id: mylegacyapp' http://10.0.0.5:8080/example/api

The first request, by a modern app, should hit http://example.demo.sensedia.com/example/api. The second request, by a legacy app, should be routed to http://legacy.backend.com.

Convert the response body from JSON to XML

In this example, we have an API at the backend which returns a JSON response, but we want to set up an equivalent XML to return to the client.

The default interceptor JSON to XML can be used to carry out this transformation.

This script must be executed in the response flow.

// Parses the JSON received
var json = JSON.parse( $call.response.getBody().getString("utf-8") );

// This function converts an object into an XML representation.
// Note that it does not deal with sub-objects or collections
var toXml = function(jsonNode, rootName) {

    // We are using JDOM to set up the XML
    var result = new Packages.org.jdom.Element(rootName);

    // Each jsonNode property becomes a sub-element of the element above
    for ( var key in jsonNode ) {
        var value = jsonNode[key];

        var element = new Packages.org.jdom.Element(key);
        element.setText(value.toString());
        result.addContent(element);
    };
    return result;
};

// Triggers the filter at the root node
// Note that we are assuming that the result is always an object, not a list
var root = toXml( json, "root" );

// Serializes and sets the response
$call.response.getBody().setString( $jdom.stringify(root), "utf-8" );

With a bit more code, we can make the function toXml() deal with object lists, complex objects, etc.

Convert the request body from text to JSON

This example receives a request in the properties format (a subset of YAML):

invoice : 34843
customer: Benjamin
date    : 2001-01-23
tax     : 251.42
total   : 4443.52

We want to convert it to a JSON, as the one below:

{
  "invoice": 34843,
  "customer": "Benjamin",
  "date": "2001-01-23",
  "tax": 251.42,
  "total": 4443.52
}

For such, let us execute the following script in the request flow:

// Always specify the encoding.
// In this case, we are forcing utf-8,
// but we could consult the request headers to find out
var text = $call.request.getBody().getString("utf-8");
var newBody = "{";
text.split("\n").forEach(function(line) {
    var pieces = line.split(":");
    if (pieces[0].trim() == 'invoice' || pieces[0].trim() == 'tax' || pieces[0].trim() == 'total')
        newBody += '"' + pieces[0].trim() + '"' + ':' + pieces[1].trim() + ',';
    else
        newBody += '"' + pieces[0].trim() + '"' + ':' + '"' + pieces[1].trim() + '",';
});
if ( newBody.length > 1 )
    newBody = newBody.substring(0,newBody.length -1);
newBody += '}';

// Sets the new request body
$call.request.getBody().setString(newBody, "utf-8");
$call.request.setHeader("Content-Type", "application/json");
The default interceptor TXT to JSON can be used to carry out this transformation.

Create a XML and add to the response

This script creates a XML using JDOM and adds it to the response.

var result = new org.jdom.Element("sensedia");
result.setText("xml");
$call.response.getBody().setString( $jdom.stringify(result), "utf-8" );

Unpack the request body with gzip

The following code unpacks the request body compressed using gzip.

This transformation must be applied to the request flow.

var decompressed = $gzip.decompress( $call.request.getBody().getBytes() );
var json = JSON.parse(decompressed);
json.test = 2;
$call.request.getBody().setString( JSON.stringify(json) , "utf-8" );

Execute other services during transformation

The best way to execute other services is running the Service Mashup interceptor. However, it is also possible to execute them with custom interceptors.

This script orchestrates services, which can be called in any flow (request or response).

    if($http.get("http://sensedia.com").status == 200)
    {
        $http.get("http://sensedia.com/blog")
    }



    if($http.get("http://sensedia.com").status == 200)
    {
        var header = { 'Content-Type' : 'application/json' }
        var body = { "hello": "world" }
        $http.post("http://www.mocky.io/v2/551018c499386d1a0b53b04b", header, body)
    }

Interrupt the request flow and return the response to the client

In this example, a new attribute was included in the object $call, enabling the interruption of the request flow at each point, thus returning the response to the client.

To stop the request and return the response without going to the backend and executing the response flow, add this script:

$call.stopFlow = true;
$call.decision.setAccept( false );
$call.response = new com.sensedia.interceptor.externaljar.dto.ApiResponse();
$call.response.setStatus( 400 );

To interrupt the request or response, making the request to the backend:

$call.stopFlow = true;

Handle billing

This is a script to handle billing.

$call.billing = true;
$call.billingValue = 5.85;
$billing.execute($call);

Observations:

  • The attribute billingValue works with Double precision, so it might be rounded or lose accuracy in decimal places.

  • For consumption control and blocking, select the Abort request if fail option on the editor, besides including exception handling in the custom interceptor itself or in the Raise Exception in the flow.

Handle body

Rename a field in the request body

var json = JSON.parse( $call.request.getBody().getString( "utf-8" ) );
if ( json['fatherId'] != null ) {
    json['parentId'] = json['fatherId'];
    delete json['fatherId'];
}

$call.request.getBody().setString( JSON.stringify(json), "utf-8" );

Convert the JSON response body to XML

var json = JSON.parse($call.response.getBody().getString("utf-8"));

var toXml = function (jsonNode, rootName) {
    var result = new Packages.org.jdom.Element(rootName);
    for (var key in jsonNode) {
        var value = jsonNode[key];

        if (value) {
            var element = new Packages.org.jdom.Element(key);
            element.setText(value.toString());
            result.addContent(element);
        }
    };
    return result;
};

var root = toXml(json, "root");
$call.response.getBody().setString($jdom.stringify(root), "utf-8");

Convert the request body from text to JSON

var text = $call.request.getBody().getString("utf-8");
var newBody = "{";
text.split("\n").forEach(function(line) {
    var pieces = line.split(":");
    if (pieces[0].trim() == 'invoice' || pieces[0].trim() == 'tax' || pieces[0].trim() == 'total')
        newBody += '"' + pieces[0].trim() + '"' + ':' + pieces[1].trim() + ',';
    else
        newBody += '"' + pieces[0].trim() + '"' + ':' + '"' + pieces[1].trim() + '",';
});
if ( newBody.length
>
 1 )
    newBody = newBody.substring(0,newBody.length -1);
newBody += '}';

$call.request.getBody().setString(newBody, "utf-8");
$call.request.setHeader("Content-Type", "application/json");

Obtain a parameter from the URI and add it to the request body

if ( $call.request.getQueryParam("userId") != null )
{
    var json = JSON.parse($call.request.getBody().getString("utf-8"));
    json["userId"] = String($call.request.getQueryParam("userId"));

    $call.request.getBody().setString(JSON.stringify(json), "utf-8");
    $call.request.setHeader("Content-Type", "application/json");
}

Unpack the gzip request body

var decompressed = $gzip.decompress( $call.request.getBody().getBytes() );
var json = JSON.parse(decompressed);
json.test = 2;
$call.request.getBody().setString( JSON.stringify(json) , "utf-8" );

Transformation with JSON sub-elements and JSON array

var json = JSON.parse( $call.response.getBody().getString("utf-8") );

var toXml = function(jsonNode, elementName) {
    var result = new Packages.org.jdom.Element(elementName);
    for ( var key in jsonNode ) {
        var value = jsonNode[key];
        if(value != null && value != 'null' && value != undefined){
            if(value.toString().indexOf('[object Object]') == -1 ){
                var element = new Packages.org.jdom.Element(key);
                element.setText(value.toString());
                result.addContent(element);
            }else{
                try{
                    var subElement = toXml(value, key);
                    result.addContent(subElement);
                }catch(err){
                    try{
                        for ( var keyArray in value ) {
                            var valueArray = value[keyArray];
                            var subElementArray = toXml(valueArray, key);
                            result.addContent(subElementArray);
                        }
                    }catch(err){
                            var elementError = new Packages.org.jdom.Element(key);
                            elementError.setText(err);
                            result.addContent(elementError);
                    }
                }
            }
        }
    };
    return result;
}

var root = toXml( json, "root" );

$call.response.getBody().setString( $jdom.stringify(root), "utf-8" );
$call.response.setHeader("Content-Type", "application/xml");

Handle header

Add a fixed header (Content-Type)

if ( $call.request.getHeader( "Content-Type" ) == null )
    $call.request.setHeader( "Content-Type", "application/json" );

Add header from the extraInfo of an app and access token

var access_token = $call.accessToken;
if ( access_token != null )
    $request.setHeader( "X-Customer-Id", access_token.extraInfo.get('customerId') );

var app = $call.app;
if ( app != null && "true" == app.extraInfo.get("isSandbox") )
    $request.setHeader( "X-Is-Sandbox", "true" );

Modify header authorization

var auth = $call.request.getHeader( "Authorization" )
if ( auth != null ) {

    if ( auth.toLowerCase().indexOf( "basic" ) == 0 )
        auth = auth.substring("Basic".length).trim();

    var plain = $base64.atob(auth);
    var withoutWhitespace = plain.trim();
    var base64 = $base64.btoa(withoutWhitespace);

    $call.request.setHeader( "Authorization", base64 );
}

Call a service that returns 302 and join the response headers with the headers of the next service to resume the Client

var destination = $call.destinationUri.toString();

var requestHeaders = {};

var requestHeaderskeyArray = $call.request.getAllHeaderNames().toArray();
for( var i = 0; i < requestHeaderskeyArray.length; i++ ) {

    var key = requestHeaderskeyArray[i];

    if ( key != 'host' && key != 'content-length' ) {
        requestHeaders[key] = $call.request.getHeader(key);
    }

}

$call.response = new com.sensedia.interceptor.externaljar.dto.ApiResponse();
$call.decision.setAccept( false );

var firstResponse = $http.post( destination, requestHeaders, $call.request.getBody().getString( 'UTF-8' ) );

var firstResponseHeaderskeyArray = firstResponse.getHeaders().keySet().toArray();
for( var i = 0; i < firstResponseHeaderskeyArray.length; i++ ) {

    var key = firstResponseHeaderskeyArray[i];
    $call.response.setHeader( key, firstResponse.getHeaders().get( key ) );

}

if ( firstResponse.getStatus() == 302 ){

    destination = firstResponse.getHeaders().get( 'Location' );
    var secondResponse = $http.post( destination, requestHeaders, $call.request.getBody().getString( 'UTF-8' ) );

    var secondResponseHeaderskeyArray = secondResponse.getHeaders().keySet().toArray();
    for( var i = 0; i < secondResponseHeaderskeyArray.length; i++ ) {

        var key = secondResponseHeaderskeyArray[i];
        $call.response.setHeader( key, secondResponse.getHeaders().get( key ) );

    }

    $call.response.setStatus( secondResponse.getStatus() );

    $call.response.getBody().setString( secondResponse.responseText, 'UTF-8' );

} else {

    $call.response.setStatus( firstResponse.getStatus() );

    $call.response.getBody().setString( firstResponse.responseText, 'UTF-8' );

}

Remove a request header attribute before forwarding the call to the backend

try{
   $call.request.getAllHeaders().remove("seu-header");
} catch(e){
   $call.tracer.trace(e);
}

Handle extraInfo

Validate URL against extraInfo

In this example, we will create a script to be executed only for the URLs of the type /projects/*. This pattern must be inserted in the field URL Pattern.

This API returns a list of members of a given project. However, each access token can only see certain projects. The list of allowed projects is stored in the token’s extraInfo, at the moment of registration.

access_token.extraInfo = new Packages.java.util.HashMap();
access_token.extraInfo.put('allowedProjects', 'marketing-site,product-support');

If the token above is used to call /projects/marketing-site or /projects/product-support, it must be allowed access; but if it tried to call /projects/ui-redesign, we want it to be barred with an "HTTP 403 Forbidden" error and a message providing explanation.

As we need the token extracted and validated, we will insert the script in the request flow:

var accessToken = $call.accessToken;
var extraInfo = accessToken.extraInfo;

// The value is a list separated by commas
var allowedProjects = ( extraInfo.get('allowedProjects') || "*" ).split(",");
var allowedCall = false;

// Verifies if a called URL matches an allowed project
var url = $call.request.getRequestedUrl().getPath();
for ( var i = 0; i < allowedProjects.length; i++ ) {

    // An asterisk means that this token can be used for any project
    if ( allowedProjects[i] == "*" ){
        allowedCall = true;
        break;
    }

    // Verifies if the URL is allowed.
    // If so, leave the script without any alteration
    if ( url.startsWith("/projects/" + allowedProjects[i]) ){
        allowedCall = true;
        break;
    }
}

if ( !allowedCall ){
    // Arrives here if the URL called is from no allowed project
    $call.decision.setAccept( false );
    $call.response.setStatus( 403 );
    var body = {
        message: "This token can only be used for the following projects: " + allowedProjects.join(",")
    };

    // Note the use of the utility JSON.stringfy(),
    // which returns a string with the JSON representation of an object.
    $call.response.getBody().setString( JSON.stringify(body), "utf-8" );
}

With this script and token, these are the expected results:

GET /projects/marketing-site               => HTTP 200
GET /projects/product-support              => HTTP 200
GET /projects/ui-redesign                  => HTTP 403

Obtain token from extraInfo

In the example below, we have some legacy clients that cannot modify their systems to send oAuth 2.0 access tokens. Instead, they need to keep on sending a username/password pair in a proprietary header X-User-Password. We want to recover this header, extract the data, and set up a new Authorization header with codes that we already know.

For the Gateway, everything happens as if the legacy system had sent an Authorization header, as the other systems. Therefore, this script needs to be executed in the request flow. As we know that only three legacy systems are working this way, we will perform a hard-coded validation of the credentials.

var oldHeader = $call.request.getHeader("X-User-Password");
oldHeader = $base64.atob(oldHeader);

var legacySystems = {
    "system1:password1": "abc123",
    "system2:password2": "def456",
    "system3:password3": "ghi789"
}

var correctToken = legacySystems[oldHeader];
if ( correctToken == null ) {
    $call.decision.setAccept( false );
    $call.response.setStatus( 403 );
    var body = { message: "Bad username/password" };
    $call.response.getBody().setString( JSON.stringify(body), "utf-8" );
} else {
    $call.request.setHeader( "Authorization", "Basic " + correctToken );
}

Change route according a token’s extraInfo

var extraInfo = $call.accessToken.extraInfo || new Packages.java.util.HashMap();
var isLegacy = extraInfo.get("isLegacy");

if ( isLegacy == "true" ) {
    var newDest = 'http://legacy.backend.com';
    var newUrl = new java.net.URI(newDest)
    $call.setDestinationUri(newUrl);
    $call.request.setHeader('Host', $call.destinationUri.getHost());
}

Compare the extralnfo value with the API resource value

try {
 var pass = false;
 var url = $call.request.getRequestedUrl().getPath().split("/v1/");
 var resource = url[1];

 //Gets extraInfos and runs through all of them, comparing their value
 //with the value of the API resources that are being consumed
 var extraInfos = $call.app.extraInfo;
 if(extraInfos){
  var extraInfosArray = extraInfos.values().iterator();
  while(extraInfosArray.hasNext()){
   if (resource == extraInfosArray.next()){
    pass = true;
   }
  }

 }
 if(!pass){
  $call.response = new com.sensedia.interceptor.externaljar.dto.ApiResponse();
  $call.decision.setAccept(false);
  $call.response.setStatus(403);
  $call.response.getBody().setString('{"message": "Forbidden"}','utf-8');
 }
} catch (e) {
 $call.tracer.trace(e);
}

Handle query params

Add pagination query params when absent

if ( ! $request.getQueryParam("page") )
    $request.setQueryParam("page", "0");

if ( ! $request.getQueryParam("page_size") )
    $request.setQueryParam("page_size", "20");

var page = parseInt($request.getQueryParam("page"));
if (page > 100 || page < 0)
    $request.setQueryParam("page", 0);

var pageSize = parseInt($request.getQueryParam("page_size"));
if (pageSize > 100 || pageSize < 1)
    $request.setQueryParam("page_size", 20);

Handle responses

Create an XML and add to the response

var result = new org.jdom.Element("sensedia");
result.setText("xml");
$call.response.getBody().setString( $jdom.stringify(result), "utf-8" );

Stop the execution flow in the request and return the response to the client

$call.stopFlow = true;
$call.decision.setAccept( false );
$call.response = new com.sensedia.interceptor.externaljar.dto.ApiResponse();
$call.response.setStatus( 400 );

Treat back-end exceptions

var respBody = $call.response.getBody().getString("utf-8") || "";
respBody = respBody.trim();
if ($call.response.getStatus() > 400 &&
    respBody.length() > 0 &&
    respBody[0] != '[' &&
    respBody[0] != '{') {

    respBody = JSON.stringify({message: "A server error occurred."});
}

Modify "Authorization" header

This is a script to modify an Authorization header.

In the example below, the client is sending an Authorization header with this content: Basic YWJjZGVmCg==. Decoding the Base64 value, we obtain abcdef, but with a newline (\n) at the end. We will remove the Basic part, decode the rest of the value in Base64, remove the newline, and encode again:

var auth = $call.request.getHeader( "Authorization" )
if ( auth != null ) {

    // Removes the prefix "Basic: "
    if ( auth.toLowerCase().indexOf( "basic" ) == 0 )
        auth = auth.substring("Basic".length).trim();

    // Decodes, removes the newline e encodes again.
    // Note the use of the utility $base64.
    // See other utilities in the hooks developer reference.
    var plain = $base64.atob(auth);
    var withoutWhitespace = plain.trim();
    var base64 = $base64.btoa(withoutWhitespace);

    // Overwrites the original header value
    $call.request.setHeader( "Authorization", base64 );
}

This is a script to obtain a token from a cookie. In the example below, we have a request that sends the access token as a cookie — that is, among the other cookies sent to the server. The HTTP request is as follows:

GET http://api.company.com/resources
Cookie: cookie1=value1; cookie2=value2; access_token=abcdef

Note that there are three token in the Cookie header, and we want the API Gateway’s processing to treat the request as if the abcdef AccessToken had been sent in an access_token header. We will execute the script in the request flow, since it is during validation that the tokens will be extracted and validated.

// parseRequestCookieValues only works in the request.
// To interpret response cookies,
// see the other methods of the $cookies utility class.
// The result is a case-insensitive Map of cookie names for your values.
var allCookies = $cookies.parseRequestCookieValues($call.request.getHeader("Cookie"));
$call.request.setHeader( "access_token", allCookies.get("access_token") );

Rename a field in the request body

In this example, the client sends a request in a format that is a bit old; in the new format, a field has changed names. We only want to translate the old name into the new one:

old format:                new format:

{                          {
  "id"      : 123,           "id"      : 123,
  "fatherId": 456            "parentId": 456
}                          }

The script must be executed in the request flow, so that it runs before the call is forwarded to the backend.

var json = JSON.parse( $call.request.getBody().getString( "utf-8" ) );
if ( json['fatherId'] != null ) {
    json['parentId'] = json['fatherId'];
    delete json['fatherId'];
}

$call.request.getBody().setString( JSON.stringify(json), "utf-8" );

The script can be tested pointing to an endpoint that echoes what was received:

curl -H 'Content-Type: application/json' -d '{"fatherId": 123}' http://10.0.0.5:8080/example

The result must include the body received:

...
"body" : "{\"parentId\":123}",
...

Suppress sensitive information before sending metrics

In some cases, we have APIs that transfer confidential data between the backend and the client (such as credit card numbers) and we don’t want to expose these data, not even to system administrators that might use the API Manager to look into problems. In this example, we will use a script that runs at the before-metric point to mask these fields.

The request and the response are not modified by the script below; only the copies of these objects, sent to the Manager for storage and analysis, are affected. The backend keeps on receiving the credit card number that was send by the client, and the client keeps on receiving the number in the response.
var maskField = function(s) {
    if ( s == null ) return null;
    return "****-****-****-" + s.substring( 15 );
}

var maskFieldsInBody = function(bodyString) {
    var json = JSON.parse( bodyString );
    json["creditCard"] = maskField( json["creditCard"]  );
    return JSON.stringify( json );
}

// We modified only the data in $call.metric.
// $call.request and $call.response are not altered.
$call.metric.requestBody = maskFieldsInBody( $call.metric.requestBody );
$call.metric.responseBody = maskFieldsInBody( $call.metric.responseBody );

Transform methods: PUT to GET

if ( $call.request.getMethod() == "PUT" )
{
    $call.request.setMethod( "GET" );
}

Treat backend exceptions

In this example, there is a backend which, in case of errors, returns a stacktrace in HTML or simple text formats. Instead, we want to set an appropriate response to the client. We will execute the script at the after-response stage.

var respBody = $call.response.getBody().getString("utf-8") || "";
respBody = respBody.trim();
if ($call.response.getStatus() > 400 &&
    respBody.length() > 0 &&
    respBody[0] != '[' &&
    respBody[0] != '{') {

    respBody = JSON.stringify({message: "A server error occurred."});
}

Another option would be returning a standard error message inside a JSON. To do that, execute the code below at the after-response stage:

if($call.response.getStatus() == 401){
    var errorBody = $call.response.getBody().getString("utf-8");
    var jsonError = {};
    jsonError.status = String($call.response.getStatus());
    jsonError.message = String(errorBody);
    $call.response.getBody().setString(JSON.stringify(jsonError), "utf-8");
    $call.response.setHeader("Content-Type", "application/json");
}
Thanks for your feedback!
EDIT
How useful was this article to you?