karate.call() with a real HTTP request repeats the first response when called in a loop with updated params (2.1.1 → 2.1.2 regression)
Description
Calling the same feature repeatedly via karate.call(path, arg) in a loop, where arg.params changes on each iteration, always sends the first iteration's params on every subsequent call — the HTTP request never picks up the updated value. This worked correctly on 2.1.1.
Important isolating detail: the bug only reproduces when the callee does a real HTTP call (Given url / And path / And params / When method get). An equivalent pure-JS repro — a while loop calling a feature that just echoes back a value with no HTTP request (* def echoed = value) — works correctly on 2.1.2 with the identical loop/reassignment structure. So this isn't a general JS variable-reassignment bug; it's specific to how a repeated karate.call() re-resolves path/params (or builds the request) for an HTTP-calling feature.
Steps to Reproduce
Environment: io.karatelabs:karate-core / karate-js — passes on 2.1.1, fails on 2.1.2.
callee.feature:
Feature: generic GET call
Scenario: Make GET call
Given url 'http://localhost:9999'
And path path
And params params
When method get
Then status 200
caller.feature:
Feature: reproduces a stuck pagination cursor when the callee is a real HTTP call
Background:
* def paginate =
"""
function(arg, maxIterations) {
let counter = 1;
let seen = [];
while (counter <= maxIterations) {
let result = karate.call('classpath:callee.feature', arg).response;
seen.push(result.page);
if (!result._links.next) {
return seen;
}
let params = {};
let paramStr = result._links.next.href;
let qIdx = paramStr.indexOf('?');
if (qIdx !== -1) {
paramStr = paramStr.substring(qIdx + 1);
let pairs = paramStr.split('&');
for (let i = 0; i < pairs.length; i++) {
let kv = pairs[i].split('=');
params[kv[0]] = kv[1];
}
}
arg = Object.assign({}, arg, { params: params });
counter = counter + 1;
}
return seen;
}
"""
Scenario:
* def seen = paginate({ path: '/records', params: {} }, 3)
* match seen == ['first', 'second', 'third']Backing server (any server returning different bodies per ?start= works; used a plain http.server-based mock):
- GET /records → {"page":"first","_links":{"next":{"href":"/records?start=1"}}}
- GET /records?start=1 → {"page":"second","_links":{"next":{"href":"/records?start=2"}}}
- GET /records?start=2 → {"page":"third","_links":{}}
Expected: seen == ["first", "second", "third"] Actual on 2.1.2: seen == ["first", "first", "first"] — every call after the first sends the same request as the first call, regardless of the reassigned arg.
On 2.1.1, the identical feature files pass.
Workaround: rewriting the loop as recursion (each page fetched via a fresh function call instead of reassigning a loop-scoped variable) avoids the issue, which supports it being tied to re-evaluation within the same call frame/feature-runtime instance rather than the reassignment itself.
E.g.
Background:
* def paginate =
"""
function(arg, maxIterations) {
function fetchPage(pageArg, counter, seen) {
let result = karate.call('classpath:callee.feature', pageArg).response;
seen.push(result.page);
if (!result._links.next || counter >= maxIterations) {
return seen;
}
let params = {};
let paramStr = result._links.next.href;
let qIdx = paramStr.indexOf('?');
if (qIdx !== -1) {
paramStr = paramStr.substring(qIdx + 1);
let pairs = paramStr.split('&');
for (let i = 0; i < pairs.length; i++) {
let kv = pairs[i].split('=');
params[kv[0]] = kv[1];
}
}
let nextArg = Object.assign({}, pageArg, { params: params });
return fetchPage(nextArg, counter + 1, seen);
}
return fetchPage(arg, 1, []);
}
"""
Scenario:
* def seen = paginate({ path: '/records', params: {} }, 3)
* match seen == ['first', 'second', 'third']Expected Behavior
Expected: seen == ["first", "second", "third"]
Actual Behavior
Actual on 2.1.2: seen == ["first", "first", "first"] — every call after the first sends the same request as the first call, regardless of the reassigned arg.
Karate Version
2.1.2
Java Version
21.0.11
Operating System
Linux
Source: karatelabs/karate