Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
unreleased:
new features:
- >-
GH-1544 Remove sequential execution limit for pm.execution.runRequest

7.52.0:
date: 2026-02-27
new features:
Expand Down
8 changes: 4 additions & 4 deletions lib/runner/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,15 @@ _.assign(Runner.prototype, {
* the request being resolved
* @param {Number} [options.nestedRequest.hasVaultAccess] - Mutated and set by any nested or parent request
* to indicate whether vault access check has been performed.
* @param {Number} [options.nestedRequest.invocationCount] - The number of requests currently accummulated
* by the nested request chain.
* @param {Array} [options.nestedRequest.callStack] - The current stack of nested request item ids
* used to enforce max nested depth. Internally set and used.
* @param {Object} [options.requester] - Options specific to the requester
* @param {Function} [options.script.requestResolver] - Resolver that receives an id from
* pm.execution.runRequest and returns the JSON for the request collection.
* Should return a postman-collection compatible collection JSON with `item` containing the request to run,
* `variable` array containing list of request-specific-collection variables and `event` with scripts to execute.
* @param {Number} [options.maxInvokableNestedRequests] - The maximum number of nested requests
* that a script can invoke, combined in total and recursively nested
* @param {Number} [options.maxInvokableNestedRequests] - The maximum nested depth
* that a script can invoke via pm.execution.runRequest
* @param {Number} [options.iterationCount] -
* @param {CertificateList} [options.certificates] -
* @param {ProxyConfigList} [options.proxies] -
Expand Down
43 changes: 36 additions & 7 deletions lib/runner/nested-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,33 @@ function runNestedRequest ({ executionId, isExecutionSkipped, vaultSecrets, item
const self = this,
requestResolver = _.get(self, 'options.script.requestResolver'),
runRequestRespEvent = EXECUTION_RUN_REQUEST_RESPONSE_EVENT_BASE + eventId,
maxInvokableNestedRequests = _.get(self, 'options.maxInvokableNestedRequests');
maxInvokableNestedRequests = _.get(self, 'options.maxInvokableNestedRequests'),
itemId = item.id,
currentItemState = { isPoppedFromCallStack: false },
popCurrentItemFromNestingChain = () => {
if (currentItemState.isPoppedFromCallStack) { return; }

currentItemState.isPoppedFromCallStack = true;

if (!self.state.nestedRequest.callStack.length) { return; }

const { callStack } = self.state.nestedRequest,
itemIndex = callStack.lastIndexOf(itemId);

if (callStack.at(-1) === itemId) {
self.state.nestedRequest.callStack.pop();

return;
}

if (itemIndex !== -1) {
self.state.nestedRequest.callStack.splice(itemIndex, 1);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can directly use the destructured variable callStack

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I have preferred not updating the destructured variable as I am trying to ensure the mutations go to the self.state.nestedRequest object, which is shared across runs.

There was an issue in the app that gets fixed only if all mutations to nested properties go via this chain instead of a destructured variable.

}
};

function dispatchErrorToListener (err) {
popCurrentItemFromNestingChain();

const error = serialisedError(err);

delete error.stack;
Expand All @@ -28,15 +52,16 @@ function runNestedRequest ({ executionId, isExecutionSkipped, vaultSecrets, item
isNestedRequest: true,
rootCursor: cursor,
rootItem: item,
invocationCount: 0
callStack: []
});

self.state.nestedRequest.invocationCount++;
self.state.nestedRequest.callStack = self.state.nestedRequest.callStack || [];
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed even when we are setting the default [] above?

self.state.nestedRequest.callStack.push(itemId);

// No more than maxInvokableNestedRequests runRequest calls per script or any of its nested request scripts
if (self.state.nestedRequest.invocationCount > maxInvokableNestedRequests) {
return dispatchErrorToListener(new Error('The maximum number of pm.execution.runRequest()' +
' calls have been reached for this request.'));
// No more than maxInvokableNestedRequests nested depth
if (self.state.nestedRequest.callStack.length > maxInvokableNestedRequests) {
return dispatchErrorToListener(new Error('Max pm.execution.runRequest depth of ' +
maxInvokableNestedRequests + ' has been reached.'));
}

let rootRequestEventsList = self.state.nestedRequest.rootItem.events?.all?.() || [],
Expand Down Expand Up @@ -137,6 +162,8 @@ function runNestedRequest ({ executionId, isExecutionSkipped, vaultSecrets, item
variableMutationsFromThisExecution = {};

if (err) {
popCurrentItemFromNestingChain();

return self.host
.dispatch(EXECUTION_RUN_REQUEST_RESPONSE_EVENT_BASE + eventId,
requestId, err);
Expand Down Expand Up @@ -206,6 +233,8 @@ function runNestedRequest ({ executionId, isExecutionSkipped, vaultSecrets, item
delete error.stack;
}

popCurrentItemFromNestingChain();

return self.host.dispatch(runRequestRespEvent,
requestId, error || null,
responseForThisRequest,
Expand Down
243 changes: 239 additions & 4 deletions test/integration/runner-spec/run-collection-request.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,7 @@ describe('pm.execution.runRequest handling', function () {
});
});

it('should handle number of requests limitation set by opts.maxInvokableNestedRequests', function (done) {
it('should allow sequential requests even if nesting limit is passed', function (done) {
const collection = new sdk.Collection({
item: [{
event: [{
Expand All @@ -524,6 +524,9 @@ describe('pm.execution.runRequest handling', function () {
await pm.execution.runRequest('nested-request-id');
await pm.execution.runRequest('nested-request-id');
await pm.execution.runRequest('nested-request-id');
pm.test('max nested requests not enforced for sequential calls', function () {
pm.expect(true).to.be.true;
});
`
}
}],
Expand Down Expand Up @@ -553,13 +556,245 @@ describe('pm.execution.runRequest handling', function () {
maxInvokableNestedRequests: 2
},
function (_err, run) {
let exceptionEncountered;

run.start({
assertion (_cursor, assertionOutcomes) {
const reqAssertions = assertionOutcomes
.filter(function (outcome) {
return outcome.name === 'max nested requests not enforced for sequential calls';
});

reqAssertions.forEach(function (assertion) {
expect(assertion.passed).to.be.true;
});
},
exception (_cursor, err) {
expect(err.message).to.eql('The maximum number of pm.execution.runRequest()' +
' calls have been reached for this request.');
exceptionEncountered = err;
},
done (err) {
done(err);
done(err || exceptionEncountered);
}
});
});
});

it('should enforce nesting depth limit for nested requests', function (done) {
const collection = new sdk.Collection({
item: [{
id: 'root-request-id',
event: [{
listen: 'prerequest',
script: {
exec: `
await pm.execution.runRequest('nested-request-1');
`
}
}],
request: {
url: 'https://postman-echo.com/get',
method: 'GET'
}
}]
});

new collectionRunner().run(collection,
{
script: {
requestResolver (_requestId, _nestedRequestContext, callback) {
callback(null, {
item: {
id: 'nested-request-1',
event: [{
listen: 'prerequest',
script: {
exec: `
await pm.execution.runRequest('nested-request-2');
`
}
}],
request: {
url: 'https://postman-echo.com/post',
method: 'POST'
}
},
event: []
});
}
},
maxInvokableNestedRequests: 1
},
function (_err, run) {
let exceptionEncountered;

run.start({
exception (_cursor, err) {
exceptionEncountered = err;
},
done (err) {
if (err) {
return done(err);
}

const expectedErrorMessage = 'Max pm.execution.runRequest depth of 1 has been reached.';

expect(exceptionEncountered).to.be.ok;
expect(exceptionEncountered.message).to.eql(expectedErrorMessage);

done();
}
});
});
});

it('should cap recursive self runRequest calls to avoid infinite loops', function (done) {
const collection = new sdk.Collection({
item: [{
event: [{
listen: 'prerequest',
script: {
exec: `
await pm.execution.runRequest('nested-request-self');
`
}
}],
request: {
url: 'https://postman-echo.com/get',
method: 'GET'
}
}]
});

new collectionRunner().run(collection,
{
script: {
requestResolver (_requestId, _nestedRequestContext, callback) {
callback(null, {
item: {
id: 'nested-request-self',
event: [{
listen: 'prerequest',
script: {
exec: `
await pm.execution.runRequest('nested-request-self');
`
}
}],
request: {
url: 'https://postman-echo.com/post',
method: 'POST'
}
},
event: []
});
}
},
maxInvokableNestedRequests: 1
},
function (_err, run) {
let exceptions = [];

run.start({
exception (_cursor, err) {
exceptions.push(err);
},
done (err) {
if (err) { return done(err); }

expect(exceptions.length).to.be.greaterThan(0);
exceptions.forEach(function (exception) {
expect(exception && exception.message)
.to.include('Max pm.execution.runRequest depth');
});

done();
}
});
});
});

it('should cap cyclic runRequest calls to avoid infinite loops', function (done) {
const collection = new sdk.Collection({
item: [{
event: [{
listen: 'prerequest',
script: {
exec: `
await pm.execution.runRequest('nested-request-1');
`
}
}],
request: {
url: 'https://postman-echo.com/get',
method: 'GET'
}
}]
});

let exceptions = [];

new collectionRunner().run(collection,
{
script: {
requestResolver (_requestId, _nestedRequestContext, callback) {
if (_requestId === 'nested-request-1') {
return callback(null, {
item: {
id: 'nested-request-1',
event: [{
listen: 'prerequest',
script: {
exec: `
await pm.execution.runRequest('nested-request-2');
`
}
}],
request: {
url: 'https://postman-echo.com/post',
method: 'POST'
}
},
event: []
});
}

return callback(null, {
item: {
id: 'nested-request-2',
event: [{
listen: 'prerequest',
script: {
exec: `
await pm.execution.runRequest('nested-request-1');
`
}
}],
request: {
url: 'https://postman-echo.com/post',
method: 'POST'
}
},
event: []
});
}
},
maxInvokableNestedRequests: 2
},
function (_err, run) {
run.start({
exception (_cursor, err) {
exceptions.push(err);
},
done (err) {
if (err) { return done(err); }

expect(exceptions.length).to.be.greaterThan(0);
exceptions.forEach(function (exception) {
expect(exception && exception.message)
.to.include('Max pm.execution.runRequest depth');
});

done();
}
});
});
Expand Down
Loading