change the request body before calling loadbalancer/target server

I need two differents backend servers in a target server LB apigee configuration.

I need to modify the request body before send to a target server based it.

If apigee decide to call “target server a” i need to prepare request body, if apigee decide to call “target server b” another body is necessary built.

Is possible get the target url in a javascript policy to decide how prepare the request body?

Is there any other form to do that?

I’m assuming you have some condition as to which target server should be used.

I dont think you’re after the target server LB configuration but rather conditional routing and a unique target endpoint for each backend. This will then allow you to define different policies for request/response processing specific to each backend on their target endpoint configuration

https://docs.apigee.com/api-platform/fundamentals/understanding-routes#determiningtheurlofthetargetendpoint-conditionaltargets

1 Like

Hi @dknezic , the main idea to use LB is to create a testing a/b strategy, we are migrating the old backend to new backend and this way we need to keep the old request body and new request body before send the request to the backend.

I´m assuming this strategy is very hard to implement in apigee.

How i can do that?

@felipeboso I don’t think that it implementing an a/b strategy in Apigee would be very hard to do, in general. I think if you specifically want to use the Apigee Load Balancer in order to implement that strategy, it introduces some extra twists.

I think from your explanation you want to couple routing with payload modification. Something like this:

  • use a weighted random approach to select either A or B as a target. Maybe A gets 6% of load and B gets 94%. Or, you could use some other criteria to select between A and B. Maybe you use a custom attribute attached to the client or API key in Apigee, and that attributes indicates whether A or B is selected. Or maybe it’s based on time-of-day. Or the presence of a header in the inbound request. Or some combination of these things. It doesn’t matter. The point is, you’ve got a way to select the target.
  • If routing to A, then keep payload as-is. If routing to B, then modify the payload (request body) in a specific way.
  • Apigee then invokes the selected target

What Dane is saying is … You can implement this in JavaScript pretty easily.

Within a JS step, you can set target.url variable to a different setting, corresponding to either A or B being selected. This JS step MUST be attached to the target request flow.

Also within a JS step, you can perform payload modification, or if you’d rather use XSL or similar to modify the payload, then just set a context variable in JS, and then your flow can refer to that context variable to conditionally execute the XSL (or similar).

Let’s look more specifically at how you might implement a weighted random selection.

To make this happen, you need the API proxy to be able to reference a 2-element array, containing the URL for A and B, and the weights for each of A and B. It might look like this:

[
  { "name": "a", "url": "https://a.example.com", "weight": 6 },
  { "name": "b", "url": "https://b.example.com", "weight": 94 }
]

It’s an array of objects. The inner elements contains a name, a url, and a weight. The Apigee KVM is a good place to store this kind of routing table, but you could also store that in some external system, that is accessible via ServiceCallout. If you use the latter you will want to wrap the callout with a cache. It’s not important that the weights add up to 100, of course. Your weighted random selector logic can easily normalize.

OK, then you have JS that looks something like this:

var routingTable = JSON.parse(context.getVariable('variable-containing-routing-table'));
var wrs = new WeightedRandomSelector(routingTable);
var selectedRoute = wrs.select(); // returns one of the items in the table
context.setVariable('target.url', selectedRoute.url);

// Option 1: modifying payload within this JS
if (selectedRoute.name == "a") {
  var p = JSON.parse(context.getVariable('request.content'));
  // example here only
  p.additionalElement = "foo";
  delete p.unwantedElement;
  context.setVariable('request.content', JSON.stringify(p));
}

// Option 2: modifying payload externally (some other step)
context.setVariable('selected_route', selectedRoute.name);

In the Option 1 case, you just need the one JS step in the target request flow. Like this:

<TargetEndpoint name="target-1">
  <Description/>
  <FaultRules/>
  <PreFlow name="PreFlow">
    <Request> 
      <Step>
        <Name>JS-SelectRoute</Name>
      </Step>
    </Request>
    <Response/>
  </PreFlow>
   ...

In the Option 2 case, you need the JS step, and the additional step wrapped in a Condition.

<TargetEndpoint name="target-1">
  <Description/>
  <FaultRules/>
  <PreFlow name="PreFlow">
    <Request> 
      <Step>
        <Name>JS-SelectRoute</Name>
      </Step>
      <Step>
        <Name>XSL-ModifyPayload</Name>
        <Condition>selected_route = "a"</Condition> 
      </Step>
    </Request>
    <Response/>
  </PreFlow>
   ...

That will work. The only remaining unsolved part is the weighted random selector. Find an example of that here. Of course your criteria for selection might be more complicated than that (time of day, custom attribute, etc). And … in that case, implementing that selector is up to you.

1 Like

@dchiesa1 I tested your solution and works pretty good.

In my case i have two servers

Server 1 - fixed token.

Server 2 - token with keycloak auth and expiration.

I use the route rule after execute the js script and condition to choose the correct server.

My files as a sample:

Javascript with routing-table with weightRandom strategy, this file is in proxy endpoint preflow.

(function (){

  function WeightedRandomSelector(a) {
    var i, L;
    this.totalWeight = 0;
    this.a = a;
    this.selectionCounts = [];
    this.weightThreshold = [];
    for (i = 0, L = a.length; i<L; i++) {
      this.totalWeight += a[i].weight;
      this.weightThreshold[i] = this.totalWeight;
      this.selectionCounts[i] = 0;
    }
  }

  WeightedRandomSelector.prototype.select = function() {
    var R = Math.floor(Math.random() * this.totalWeight),
        i, L;

    for (i = 0, L = this.a.length; i < L; i++) {
      if (R < this.weightThreshold[i]) {
        this.selectionCounts[i]++;
        return(this.a[i]);
      }
    }
    return this.a[L - 1];
  };

  if (typeof exports === "object" && exports) {
    exports.WeightedRandomSelector = WeightedRandomSelector;
  }
  else {
    var globalScope = (function(){ return this; }).call(null);
    globalScope.WeightedRandomSelector = WeightedRandomSelector;
  }

}());

var routingTable = [
   { "name" : "server1", "url": "https://server1.com", "weight": 94 },
   { "name" : "server2", "url": "https://server2.com", "weight": 6 }
];
var wrs = new WeightedRandomSelector(routingTable);
var selectedRoute = wrs.select();
context.setVariable('target.url', selectedRoute.url);

print(selectedRoute);

if (selectedRoute.name == "server1") {
  var p = JSON.parse(context.getVariable('request.content'));
  p.additionalElement = "foo";
  delete p.unwantedElement;
  context.setVariable('request.content', JSON.stringify(p));
}

context.setVariable('selected_route', selectedRoute.name);

Proxy endpoint

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ProxyEndpoint name="default">
    <PreFlow name="PreFlow">
        <Request>
            <Step>
                <Name>Spike-Arrest</Name>
            </Step>
            <Step>
                <Name>Token-Validation</Name>
            </Step>
            <Step>
                <Name>Assign-Message-Proxy-Request</Name>
            </Step>
            <Step>
                <Name>JavaScript-3</Name>
            </Step>
        </Request>
        <Response/>
    </PreFlow>
    <Flows/>
    <PostFlow name="PostFlow">
        <Request/>
        <Response>
            <Step>
                <Name>Assign-Default-Trace-ID</Name>
            </Step>
            <Step>
                <Name>Proxy-Response</Name>
            </Step>
        </Response>
    </PostFlow>
    <DefaultFaultRule name="default-fault-rule">
        <Step>
            <Name>Assign-Message-Proxy-Request</Name>
            <Condition>_log.proxy.request.method == null</Condition>
        </Step>
        <Step>
            <Name>Assign-Unauthorized-Error-Message</Name>
            <Condition>message.status.code == 401</Condition>
        </Step>
        <Step>
            <Name>Assign-Too-Many-Requests-Error-Message</Name>
            <Condition>fault.name == "SpikeArrestViolation"</Condition>
        </Step>
        <Step>
            <Name>Assign-Default-Error-Message</Name>
            <Condition>message.status.code != 401 and message.status.code != 429 and customError.code == null</Condition>
        </Step>
        <Step>
            <Name>Assign-Default-Trace-ID</Name>
        </Step>
        <Step>
            <Name>MessageLogging</Name>
        </Step>
        <AlwaysEnforce>true</AlwaysEnforce>
    </DefaultFaultRule>
    <HTTPProxyConnection>
        <BasePath>/v1/robin</BasePath>
        <Properties/>
        <VirtualHost>default</VirtualHost>
        <VirtualHost>secure</VirtualHost>
    </HTTPProxyConnection>
    <RouteRule name="server1">
        <Condition>selected_route = "server1"</Condition>
        <TargetEndpoint>server1</TargetEndpoint>
    </RouteRule>
    <RouteRule name="default">
        <TargetEndpoint>default</TargetEndpoint>
    </RouteRule>
    <RouteRule name="noroute"/>
</ProxyEndpoint>

Thanks for your help!

I’m glad it’s helpful for you.

Just some feedback in case you were not aware. You probably don’t need some of the JS code.

...
// omit this - you're not setting target.url 
context.setVariable('target.url', selectedRoute.url);

// omit this, this is for diagnostics only
print(selectedRoute);

// omit this block - you don't want to modify the payload
if (selectedRoute.name == "server1") {
  var p = JSON.parse(context.getVariable('request.content'));
  p.additionalElement = "foo";
  delete p.unwantedElement;
  context.setVariable('request.content', JSON.stringify(p));
}

...

You also don’t need the actual URLs in the routing table, of course. It seems that you’re using multiple TargetEndpoints, and the URLs are specified THERE. There’s no need to specify them in the JavaScript.

var routingTable = [
   { "name" : "server1", "weight": 94 },
   { "name" : "server2", "weight": 6 }
];

And finally, you can extract the WeightedRandomSelector thing out of your own code. Use a policy like this:

<Javascript name='JS-SelectRoute' >
  <IncludeURL>jsc://WeightedRandomSelector.js</IncludeURL> 
  <ResourceURL>jsc://selectRoute.js</ResourceURL>
</Javascript>

And in the WeightedRandomSelector.js you can include this code

(function (){

  function WeightedRandomSelector(a) {
    var i, L;
    this.totalWeight = 0;
    this.a = a;
    this.selectionCounts = [];
    this.weightThreshold = [];
    for (i = 0, L = a.length; i<L; i++) {
      this.totalWeight += a[i].weight;
      this.weightThreshold[i] = this.totalWeight;
      this.selectionCounts[i] = 0;
    }
  }

  WeightedRandomSelector.prototype.select = function() {
    var R = Math.floor(Math.random() * this.totalWeight),
        i, L;

    for (i = 0, L = this.a.length; i < L; i++) {
      if (R < this.weightThreshold[i]) {
        this.selectionCounts[i]++;
        return(this.a[i]);
      }
    }
    return this.a[L - 1];
  };

  if (typeof exports === "object" && exports) {
    exports.WeightedRandomSelector = WeightedRandomSelector;
  }
  else {
    var globalScope = (function(){ return this; }).call(null);
    globalScope.WeightedRandomSelector = WeightedRandomSelector;
  }

}());

while in the selectRoute.js, you specify this:

var routingTable = [
   { "name" : "server1", "weight": 94 },
   { "name" : "server2", "weight": 6 }
];
var wrs = new WeightedRandomSelector(routingTable);
var selectedRoute = wrs.select();
context.setVariable('selected_route', selectedRoute.name);

If I were doing this I would want to externalize that routing table information, into a KVM or a properties file or something similar.

I’m glad it’s helpful for you.

Just some feedback in case you were not aware. You probably don’t need some of the JS code.

...
// omit this - you're not setting target.url 
context.setVariable('target.url', selectedRoute.url);

// omit this, this is for diagnostics only
print(selectedRoute);

// omit this block - you don't want to modify the payload
if (selectedRoute.name == "server1") {
  var p = JSON.parse(context.getVariable('request.content'));
  p.additionalElement = "foo";
  delete p.unwantedElement;
  context.setVariable('request.content', JSON.stringify(p));
}

...

You also don’t need the actual URLs in the routing table, of course. It seems that you’re using multiple TargetEndpoints, and the URLs are specified THERE. There’s no need to specify them in the JavaScript.

var routingTable = [
   { "name" : "server1", "weight": 94 },
   { "name" : "server2", "weight": 6 }
];

And finally, you can extract the WeightedRandomSelector thing out of your own code. Use a policy like this:

<Javascript name='JS-SelectRoute' >
  <IncludeURL>jsc://WeightedRandomSelector.js</IncludeURL> 
  <ResourceURL>jsc://selectRoute.js</ResourceURL>
</Javascript>

And in the WeightedRandomSelector.js you can include this code

(function (){

  function WeightedRandomSelector(a) {
    var i, L;
    this.totalWeight = 0;
    this.a = a;
    this.selectionCounts = [];
    this.weightThreshold = [];
    for (i = 0, L = a.length; i<L; i++) {
      this.totalWeight += a[i].weight;
      this.weightThreshold[i] = this.totalWeight;
      this.selectionCounts[i] = 0;
    }
  }

  WeightedRandomSelector.prototype.select = function() {
    var R = Math.floor(Math.random() * this.totalWeight),
        i, L;

    for (i = 0, L = this.a.length; i < L; i++) {
      if (R < this.weightThreshold[i]) {
        this.selectionCounts[i]++;
        return(this.a[i]);
      }
    }
    return this.a[L - 1];
  };

  if (typeof exports === "object" && exports) {
    exports.WeightedRandomSelector = WeightedRandomSelector;
  }
  else {
    var globalScope = (function(){ return this; }).call(null);
    globalScope.WeightedRandomSelector = WeightedRandomSelector;
  }

}());

while in the selectRoute.js, you specify this:

var routingTable = [
   { "name" : "server1", "weight": 94 },
   { "name" : "server2", "weight": 6 }
];
var wrs = new WeightedRandomSelector(routingTable);
var selectedRoute = wrs.select();
context.setVariable('selected_route', selectedRoute.name);

If I were doing this I would want to externalize that routing table information, into a KVM or a properties file or something similar.