#3030·karate

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)

Author: alinaeftenoiuCreated Sep 2, 2026Updated Sep 6, 2026
Labelsbugfixed

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