Apigee Edge API Proxy - Hypermedia Links from target server - How to deal & change response links ?

In cases where the target is providing hypermedia links, how have you made them aware of the API version? One way is passing the API version (or complete basepath) as a custom header in the request. We discussed re-writing the links in Apigee but, assuming this stays in the target, any alternate approaches ?

~~S:G:TC~~

Solved Solved
1 4 554
1 ACCEPTED SOLUTION

Giving the basepath to the backend via a special header is a reasonable idea. Any backend will have to use that header in any hyperlinks it generates.

This will work, but it means that every backend will have to be aware of this convention. As far as I know there is no standard governing how to communicate the basepath from a front-end edge service to a backend implementation service. So you'd have to sort of invent an approach. That's not a bad thing, but it means every microservice in your backend has to now be aware that it may be exposed by a front end. I don't like that lack of encapsulation.

As another reason why... suppose the front end maps different URLs from the backend, differently. Suppose the backend wants to return /foos/12345 and also /bars/678 , but the front-end (edge service) exposes them as /myfoos/12345 and /thebars/678 . For whatever reason. Now, the backend needs to be aware of that mapping, even further.

So I'd rather keep the mapping separate. And that means doing the re-writing of links inside Apigee Edge. It's like the Apache mod_proxy instruction called ProxyPassReverse. This is not so difficult to implement in Apigee Edge with a JS callout.

Like this

// fixupHrefs.js
// ------------------------------------------------------------------
//
// replace paths in hrefs in object property values.
// In an API proxy, sometimes the proxied API emits URL paths.
// To prevent leakage, these need to be fixed up.
//
// Eg, suppose the backend API is at http://example.com/rest/sonoa/.
// and suppose the API proxy is like http://api.example.com/v1/codename/.
// The API may return payloads that include hrefs like /rest/sonoa/foo/bar .
// This code replaces such hrefs with /v1/codename/foo/bar .
//
// ------------------------------------------------------------------




function replaceInProps(obj, target, replacement) {
  var type = Object.prototype.toString.call(obj), x, i, index,
      L = target.length, newObj;
  if (type === "[object Array]") {
    // iterate
    newObj = [];
    for (i=0; i<obj.length; i++) {
      x = replaceInProps(obj[i], target, replacement);
      newObj.push(x);
    }
  }
  else if (type === "[object Object]") {
    newObj = {};
    for (var prop in obj) {
      if (obj.hasOwnProperty(prop)) {
        type = Object.prototype.toString.call(obj[prop]);
        if (type === "[object String]") {
          index = obj[prop].indexOf(target);
          if (index === 0) {
            // replace
            newObj[prop] = replacement + obj[prop].substr(L);
          }
          else {
            // no replace
            newObj[prop] = obj[prop];
          }
        }
        else if (type === "[object Array]" || type === "[object Object]") {
          // recurse
          newObj[prop] = replaceInProps(obj[prop], target, replacement);
        }
        else {
          // no replacement
          newObj[prop] = obj[prop];
        }
      }
    }
  }
  return newObj;
}


var respContent,
    // target = context.getVariable('replacer.target'),
    // replacement = context.getVariable('replacer.replacement'),
    // target = properties['replacer.target'],
    // replacement = properties['replacer.replacement'],
    target = '/rest/icontrol',
    replacement = '/v1/ictrl',
    newResponse;


//context.proxyResponse.headers['X-Apigee-orig-response'] = context.proxyResponse.content;


try {
  respContent = JSON.parse(context.proxyResponse.content);
  newResponse = replaceInProps(respContent, target, replacement);
  context.proxyResponse.content = JSON.stringify(newResponse);
}
catch (exc1) {
  context.proxyResponse.headers['X-fixup-hrefs-failed'] = 1;
}


//context.proxyResponse.headers['X-Apigee-fixup'] = (new Date()).toString();


View solution in original post

4 REPLIES 4

ooooh, what are these mysterious marks in your post?!?!!?

~~S:G:TC~~

🙂 Good catch @Dino , Many people ask questions which can be public in different channels but fails to post here in community.apigee.com. I track the source of the questions just for the reference. Hopefully someday there won't be a need for these mysterious marks !

Giving the basepath to the backend via a special header is a reasonable idea. Any backend will have to use that header in any hyperlinks it generates.

This will work, but it means that every backend will have to be aware of this convention. As far as I know there is no standard governing how to communicate the basepath from a front-end edge service to a backend implementation service. So you'd have to sort of invent an approach. That's not a bad thing, but it means every microservice in your backend has to now be aware that it may be exposed by a front end. I don't like that lack of encapsulation.

As another reason why... suppose the front end maps different URLs from the backend, differently. Suppose the backend wants to return /foos/12345 and also /bars/678 , but the front-end (edge service) exposes them as /myfoos/12345 and /thebars/678 . For whatever reason. Now, the backend needs to be aware of that mapping, even further.

So I'd rather keep the mapping separate. And that means doing the re-writing of links inside Apigee Edge. It's like the Apache mod_proxy instruction called ProxyPassReverse. This is not so difficult to implement in Apigee Edge with a JS callout.

Like this

// fixupHrefs.js
// ------------------------------------------------------------------
//
// replace paths in hrefs in object property values.
// In an API proxy, sometimes the proxied API emits URL paths.
// To prevent leakage, these need to be fixed up.
//
// Eg, suppose the backend API is at http://example.com/rest/sonoa/.
// and suppose the API proxy is like http://api.example.com/v1/codename/.
// The API may return payloads that include hrefs like /rest/sonoa/foo/bar .
// This code replaces such hrefs with /v1/codename/foo/bar .
//
// ------------------------------------------------------------------




function replaceInProps(obj, target, replacement) {
  var type = Object.prototype.toString.call(obj), x, i, index,
      L = target.length, newObj;
  if (type === "[object Array]") {
    // iterate
    newObj = [];
    for (i=0; i<obj.length; i++) {
      x = replaceInProps(obj[i], target, replacement);
      newObj.push(x);
    }
  }
  else if (type === "[object Object]") {
    newObj = {};
    for (var prop in obj) {
      if (obj.hasOwnProperty(prop)) {
        type = Object.prototype.toString.call(obj[prop]);
        if (type === "[object String]") {
          index = obj[prop].indexOf(target);
          if (index === 0) {
            // replace
            newObj[prop] = replacement + obj[prop].substr(L);
          }
          else {
            // no replace
            newObj[prop] = obj[prop];
          }
        }
        else if (type === "[object Array]" || type === "[object Object]") {
          // recurse
          newObj[prop] = replaceInProps(obj[prop], target, replacement);
        }
        else {
          // no replacement
          newObj[prop] = obj[prop];
        }
      }
    }
  }
  return newObj;
}


var respContent,
    // target = context.getVariable('replacer.target'),
    // replacement = context.getVariable('replacer.replacement'),
    // target = properties['replacer.target'],
    // replacement = properties['replacer.replacement'],
    target = '/rest/icontrol',
    replacement = '/v1/ictrl',
    newResponse;


//context.proxyResponse.headers['X-Apigee-orig-response'] = context.proxyResponse.content;


try {
  respContent = JSON.parse(context.proxyResponse.content);
  newResponse = replaceInProps(respContent, target, replacement);
  context.proxyResponse.content = JSON.stringify(newResponse);
}
catch (exc1) {
  context.proxyResponse.headers['X-fixup-hrefs-failed'] = 1;
}


//context.proxyResponse.headers['X-Apigee-fixup'] = (new Date()).toString();


Great answer @Dino , +1, Thank you for the detailed explanation & example. I am sure it will be helpful for others too.