Need help with loop in loop

Hi all, I created a collection with three post requests using soap xml. When running the first request the response returns an array. The information I need for the next request I store in a variable. The different values are stored using a comma ( eg. 1,2,3). The second request should start with 1. After running the second request another array is returned. Again I store the array in a comma separated variable (eg. a,b,c). The third request should loop through (a,b,c). When finished the second request should be executed again but using value 2 from variable. The array from the response should again be used for the third request to loop through. I was wondering if this is possible and if someone can help?
Kind regards, Jur

Here is a example using sendRequest() to control the loops.

This is a bit of a challenge due to the async nature of sendRequest().

So you need to use promises and awaits to ensure the correct order of operations.

I’m using a GET request for the outerloop and a POST request for the innerloop. Both using Postman Echo to provide a working example.

const sendRequest = (req) => {
    return new Promise((resolve, reject) => {
        pm.sendRequest(req, (err, res) => {
            if (err) {
                console.log(err);
                return reject(err);
            }
            resolve(res);
        });
    });
};

(async function main() {

    const outerloop = pm.response.json().data // ["1", "2", "3"] 

    for (let i = 0; i < outerloop.length; i++) {

        let request2 = {
            url: 'https://postman-echo.com/get?test=' + outerloop[i],
            method: 'GET',
        }

        const response2 = await sendRequest(request2);
        let response2json = response2.json();  // parse response
        let outerloopValue = response2json.args.test
        console.log(outerloopValue);

        const innerloop = ["A", "B", "C"] // you would get value from response instead

        for (let i = 0; i < innerloop.length; i++) {

            let innerloopValue = innerloop[i]

            const request3 = {
                url: 'https://postman-echo.com/post',
                method: 'POST',
                header: {
                    'Content-Type': 'application/json',
                },
                body: {
                    mode: 'raw',
                    raw: JSON.stringify({ 'testValue': innerloop[i] })
                }
            };

            const response3 = await sendRequest(request3);
            pm.test(`Outerloop ${outerloopValue} Innerloop request ${innerloopValue} - status code is 200`, () => {
                pm.expect(response3).to.have.status(200);
                let response3json = response3.json();
                console.log(response3json.data.testValue)
            });
        };
    };
})();

image

Thank you for your fast reply I will try it immediately

This an an updated version of the code.

I recently found out that sendRequest() supports top-level awaits.

Which means the code can be simplified.

I still had a challenge around how to structure tests to ensure that this is robust, as tests are wrapped in a function, therefore they would no longer be a top-level await. You don’t want to run request 3 if request 2 fails.

This is my updated version with better error handling. (Which should give exactly the same result).

async function asyncCall(request, t) {
    try {
        const response = await pm.sendRequest(request);
        pm.test(t, () => {
            pm.expect(response).to.have.status(200);
        });
        return {
            'data': response,
            'status': "success"
        };
    } catch (error) {
        console.log(error);
        pm.test(t, () => {
            pm.expect(error, error.code).to.be.undefined;
        });
        return {
            "status": "error",
            "errorcode": error.code
        }
    }
};

// Request 1 - parse response and define array for outerloop
const outerLoop = pm.response.json().data // ["1", "2", "3"]

// Outerloop
for (let i = 0; i < outerLoop.length; i++) {
    let outerLoopValue = outerLoop[i];
    // define GET request
    let request2 = {
        url: 'https://postman-echo.com/get?test=' + outerLoopValue,
        method: 'GET',
    }
    // async sendRequest()
    let request2response = await asyncCall(request2, `Outerloop ${outerLoopValue} - status code is 200`);
    if (request2response.status === "success") { // only run final request if previous request is success
        console.log(request2response.data.json().args.test); // Postman Echo Response

        // Innerloop
        const innerLoop = ["A", "B", "C"] // you would get value from response instead
        for (let i = 0; i < innerLoop.length; i++) {
            let innerloopValue = innerLoop[i];
            // define POST request
            const request3 = {
                url: 'https://postman-echo.com/post',
                method: 'POST',
                header: {
                    'Content-Type': 'application/json',
                },
                body: {
                    mode: 'raw',
                    raw: JSON.stringify({ 'testValue': innerloopValue })
                }
            };
            // async sendRequest()
            let request3response = await asyncCall(request3, `Outerloop ${outerLoopValue} Innerloop request ${innerloopValue} - status code is 200`);
            if (request3response.status === "success") {
                console.log(request3response.data.json().data.testValue); // Postman Echo Response
            }
        };

    }
};

Thanks for the adjusted script. I was wondering though where to put the script? In the script tab of the collection?

In the post-response tab of the first request.

You need to run that request first to get the initial array for request 2 and store it as a variable.

In the above request, this variable is defined as “outerloop”.

1 Like

I tried the script but in the provided example the loop values are in the url. In my case the url will always be the same but the loops are in the body. Is there a way to let that work as well?

The code was just an example.

The requests are being sent to Postman Echo which just returns back what you sent.

The URL’s were updated to show the loop in effect, as its easier to screenshot than something in the body.

The example has a GET request for the outer request, and a POST request with JSON body for the inner loop to show you two different types of requests in effect.

You just need to amend the variable for request2 and request3 accordingly, so its sending the right type of request, body and headers. You will use variables for the elements that will change on each iteration\loop. The 3rd request does have a variable in the body.

If you provide example screenshots for the 2nd and 3rd requests, then I might be able help with a more specific example.

I was able to make some more progress but ended up with some issues.
When running the first request I use the code below to write specific values to an array:

// Access the array of items
let items = result[‘soapenv:Envelope’][‘soapenv:Body’][0][‘getFolderContentsResponse’][0][‘getFolderContentsReturn’][0][‘catalogContents’][0].item;

// Loop through the array and write to a variable

let concatenatedNames = items.map(item => item.fileName[0]).join(', ');

Via a console.log the following results are returned:

“Advies, BaMa, Begeleider, Beheer”

I want to loop through each value in that list. So first I want Advies then BaMa etc


When I use your example:

const outerLoop = concatenatedNames;
// Outerloop
for (let i = 0; i < outerLoop.length; i++) {
let outerLoopValue = outerLoop[i]

When I use the outerLoopValue only the first letter is used instead of the complete value: “A” instead of “Advies”

let request2 = {
url: http://${serverPort}/xmlpserver/services/v2/CatalogService,
method: ‘POST’,
header: {
‘Soapaction’: ‘http://xmlns.oracle.com/oxp/servce/v2/ReportService/runReportRequest’,
},
body: {
mode: ‘raw’,
raw: <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:v2="http://xmlns.oracle.com/oxp/service/v2"> <soapenv:Header/> <soapenv:Body> <v2:getFolderContents> <v2:folderAbsolutePath>/osi01/Rapporten/${outerLoopValue}</v2:folderAbsolutePath> <v2:userID>${userName}</v2:userID> <v2:password>${passWord}</v2:password> </v2:getFolderContents> </soapenv:Body> </soapenv:Envelope>
}
};

This is a single string. The length method is available to string as it is to arrays, but they have completely different purposes. On a string, it just tells you how long your string is, on an array, it tells you how many items you have in your array. As you are getting the length of the string, its just going to loop one letter at a time (as you have found out).

Why are you joining the names? If the following creates an array, that is what you should be using in your loop.

let items = result[‘soapenv:Envelope’][‘soapenv:Body’][0][‘getFolderContentsResponse’][0][‘getFolderContentsReturn’][0][‘catalogContents’][0].item;

It should be an array like the following


["Advies","BaMa","Begeleider","Beheer"]

If you can share an example response, I might be able to help you with the formatting.

Please use the preformatted text option in the editor when pasting code or XML/JSON. It stops everything being treated as a paragraph and being aligned to the left.

Oh, I understand what you’re saying. It would be great if you can help with the formatting. I added a sample below.
Thanks in advance!

<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <soapenv:Body>
        <getFolderContentsResponse xmlns="http://xmlns.oracle.com/oxp/service/v2">
            <getFolderContentsReturn>
                <catalogContents>
                    <item>
                        <absolutePath>/osi01/Rapporten/Advies</absolutePath>
                        <creationDate>2024-08-21T19:06:15.467Z</creationDate>
                        <description xsi:nil="true"/>
                        <displayName>Advies</displayName>
                        <fileName>Advies</fileName>
                        <lastModified>2024-08-21T19:06:15.467Z</lastModified>
                        <lastModifier></lastModifier>
                        <owner></owner>
                        <parentAbsolutePath>/osi01/Rapporten</parentAbsolutePath>
                        <type>Folder</type>
                    </item>
                    <item>
                        <absolutePath>/osi01/Rapporten/BaMa</absolutePath>
                        <creationDate>2024-08-21T19:06:15.467Z</creationDate>
                        <description xsi:nil="true"/>
                        <displayName>BaMa</displayName>
                        <fileName>BaMa</fileName>
                        <lastModified>2024-08-21T19:06:15.467Z</lastModified>
                        <lastModifier></lastModifier>
                        <owner></owner>
                        <parentAbsolutePath>/osi01/Rapporten</parentAbsolutePath>
                        <type>Folder</type>
                    </item>
                    <item>
                        <absolutePath>/osi01/Rapporten/Begeleider</absolutePath>
                        <creationDate>2024-08-21T19:06:15.471Z</creationDate>
                        <description xsi:nil="true"/>
                        <displayName>Begeleider</displayName>
                        <fileName>Begeleider</fileName>
                        <lastModified>2024-08-21T19:06:15.471Z</lastModified>
                        <lastModifier></lastModifier>
                        <owner></owner>
                        <parentAbsolutePath>/osi01/Rapporten</parentAbsolutePath>
                        <type>Folder</type>
                    </item>
                </catalogContents>
            </getFolderContentsReturn>
        </getFolderContentsResponse>
    </soapenv:Body>
</soapenv:Envelope>

Your code will work as long as you remove the join when you map the file names. The map command will return an array, and this is what you use to control your loop.

The following example is pretty much the same as what you’ve already got apart from using xml2js which cleans up the XML which makes it easier to work with.

// step 1 - parse XML\Soap response 
const xmlResponse = pm.response.text();

// step 2 - parse the XML test to JSON - so its easier to work with
let parseString = require('xml2js').parseString;
const stripPrefix = require('xml2js').processors.stripPrefix;

parseString(xmlResponse, {
    tagNameProcessors: [stripPrefix],
    ignoreAttrs: true,
    explicitArray: false,
}, function (err, jsonData) {
    // handle any errors in the XML
    if (err) {
        console.log(err);
    } else {
        // do something with the result
        console.log(jsonData);
        // step 3 - create an array of the item file names
        let items = jsonData.Envelope.Body.getFolderContentsResponse.getFolderContentsReturn.catalogContents.item;
        let fileNameArray = items.map(item => item.fileName);
        console.log(fileNameArray);
        // step 4 - your outerloop
        for (let i = 0; i < fileNameArray.length; i++) {
            console.log(fileNameArray[i]);
            // step 5 - put your sendRequest code here.
        }
    }
});

With the associated console logs, so you can see what we are working with. I’ll let you add the sendRequest() to step 5.

I managed to make some big steps en got a first version running. But I encountered some errors and I am not entirely sure if it’s executing everything correctly. Below I added the code I have so far:

// step 0 - setup prerequisites
async function asyncCall(request, t) {
    try {
        const response = await pm.sendRequest(request);
        pm.test(t, () => {
            pm.expect(response).to.have.status(200);
        });
        return {
            'data': response,
            'status': "success"
        };
    } catch (error) {
        console.log(error);
        pm.test(t, () => {
            pm.expect(error).to.have.property('code').that.is.undefined;
        });
        return {
            "status": "error",
            "errorcode": error.code
        };
    }
}

pm.test("Status code is 200", function () {
    pm.response.to.have.status(200);
});

const serverPort = pm.environment.get('SERVER_PORT');
const userName = pm.environment.get('USERNAME');
const passWord = pm.environment.get('PASSWORD');
const medewerker = pm.environment.get('MEDEWERKER');
let parseString = require('xml2js').parseString;
const stripPrefix = require('xml2js').processors.stripPrefix;

// step 1 - parse XML\Soap response 
const xmlResponse = pm.response.text();

// step 2 - parse the XML test to JSON - so its easier to work with
parseString(xmlResponse, {
    tagNameProcessors: [stripPrefix],
    ignoreAttrs: true,
    explicitArray: false,
}, async function (err, jsonData) {
    // handle any errors in the XML
    if (err) {
        console.log(err);
    } else {
        // do something with the result
        console.log(jsonData);
        // step 3 - create an array of the item file names
        let items = jsonData.Envelope.Body.getFolderContentsResponse.getFolderContentsReturn.catalogContents.item;
        let fileNameArray = items.map(item => item.fileName);
        console.log(fileNameArray);
        // step 4 - your outerloop
        for (let i = 0; i < fileNameArray.length; i++) {
            console.log(fileNameArray[i]);
            // step 5 - put your sendRequest code here.
            let request2 = {
                url: `http://${serverPort}/xmlpserver/services/v2/CatalogService`,
                method: 'POST',
                header: {
                    'Soapaction': 'http://xmlns.oracle.com/oxp/servce/v2/ReportService/runReportRequest',
                    'Content-Type': 'application/xml',
                    'Accept' : '*/*'
                },
                body: {
                    mode: 'raw',
                    raw: `<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:v2="http://xmlns.oracle.com/oxp/service/v2">
   <soapenv:Header/>
   <soapenv:Body>
      <v2:getFolderContents>
         <v2:folderAbsolutePath>/osi01/Rapporten/${fileNameArray[i]}</v2:folderAbsolutePath>
         <v2:userID>${userName}</v2:userID>
         <v2:password>${passWord}</v2:password>
      </v2:getFolderContents>
   </soapenv:Body>
</soapenv:Envelope>`
                }
            }
            // async sendRequest()
            let request2response = await asyncCall(request2, `Outerloop ${fileNameArray[i]} - status code is 200`);
            if (request2response.status === "success") { // only run final request if previous request is success
               let xmlResponse2 = request2response.data.text()
               parseString(xmlResponse2, {
    tagNameProcessors: [stripPrefix],
    ignoreAttrs: true,
    explicitArray: false,
}, async function (err, jsonData2) {
    // handle any errors in the XML
    if (err) {
        console.log(err);
    } else {
        // do something with the result
        console.log(jsonData2);
        // create an array of the item file names
        let items2 = jsonData2.Envelope.Body.getFolderContentsResponse.getFolderContentsReturn.catalogContents.item;
        let absolutePathArray = items2.map(item => item.absolutePath);
        console.log(absolutePathArray);
      
               // Innerloop
            for (let j = 0; j < absolutePathArray.length; j++) {
                let innerloopValue = absolutePathArray[j];
                console.log(innerloopValue)
                // define POST request
                const request3 = {
                    url: 'http://portugal.caci.local:9704/xmlpserver/services/v2/ReportService',
                    method: 'POST',
                    header: {
                        'Soapaction': 'http://xmlns.oracle.com/oxp/servce/v2/ReportService/runReportRequest',
                    'Content-Type': 'application/xml',
                    'Accept' : '*/*'
                    },
                    body: {
                        mode: 'raw',
                        raw: `<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:v2="http://xmlns.oracle.com/oxp/service/v2">
   <soapenv:Header/>
   <soapenv:Body>
      <v2:runReport>
         <v2:reportRequest>
            <v2:attributeFormat>pdf</v2:attributeFormat>
            <v2:attributeTemplate>Standaard</v2:attributeTemplate>
            <v2:byPassCache>true</v2:byPassCache>
            <v2:flattenXML>false</v2:flattenXML>
            <v2:parameterNameValues>
               <v2:listOfParamNameValues>
                  <!-- rapport parameters, zie parameters bij rapport in de BIP omgeving (onder Bewerken) -->
                  <v2:item>
                     <v2:name>p_aanvraagnummer</v2:name>                     
                     <v2:values>
                        <v2:item>59057</v2:item>
                     </v2:values>
                  </v2:item>
                  
                   <v2:item>
                     <v2:name>p_medewerker</v2:name>                     
                     <v2:values>
                        <v2:item>${medewerker}</v2:item>
                     </v2:values>
                  </v2:item>
                  
                  <v2:item>
                     <v2:name>p_faculteit</v2:name>                     
                     <v2:values>
                        <v2:item>FEM</v2:item>
                     </v2:values>
                  </v2:item>
                  
                  <v2:item>
                     <v2:name>p_applicatie</v2:name>                     
                     <v2:values>
                        <v2:item>OSIRIS</v2:item>
                     </v2:values>
                  </v2:item>
                  
                  <v2:item>
                     <v2:name>p_taal</v2:name>                     
                     <v2:values>
                        <v2:item>NL</v2:item>
                     </v2:values>
                  </v2:item>
                  
                  <v2:item>
                     <v2:name>p_voorblad</v2:name>                     
                     <v2:values>
                        <v2:item>J</v2:item>
                     </v2:values>
                  </v2:item>
                  
                  <v2:item>
                     <v2:name>p_from</v2:name>                     
                     <v2:values>
                        <v2:item>/**/</v2:item>
                     </v2:values>
                  </v2:item>
                  
                  <v2:item>
                     <v2:name>p_where</v2:name>                     
                     <v2:values>
                        <v2:item><![CDATA[AND ROWNUM=1]]></v2:item>
                     </v2:values>
                  </v2:item>
                  
                  <v2:item>
                     <v2:name>p_orderby</v2:name>
                     <v2:values>
                        <v2:item></v2:item>
                     </v2:values>
                  </v2:item>
               </v2:listOfParamNameValues>
            </v2:parameterNameValues>
            <v2:reportAbsolutePath>${innerloopValue}</v2:reportAbsolutePath>
            <v2:sizeOfDataChunkDownload>-1</v2:sizeOfDataChunkDownload>
         </v2:reportRequest>
         <v2:userID>${userName}</v2:userID>
         <v2:password>${passWord}</v2:password>
      </v2:runReport>
   </soapenv:Body>
</soapenv:Envelope>`
                    }
                };
                // async sendRequest()
                let request3response = await asyncCall(request3, `Outerloop ${fileNameArray[i]} Innerloop request ${innerloopValue} - status code is 200`);
                if (request3response.status === "success") {
                    
                }
            }
        }
});
    }
        }
}
});

As you can see I get an error I don’t understand and I have the feeling it’s not looping through everything. In the example above the filename ‘Advies’ returned multiple xdo’s. It should execute them all but as far as I can see it stops after the first two. It doesn’t matter by the way if the posts with the xdo’s fail. I would like an overview which xdo’s failed so I can them inspect them later on


Console log “jsonData2” and “items2”.

The map function is not working so its probably because items2 is not an array.

Strange. I basically execute the same post as the initial and there I used the same syntax. In the screenshot an array is shown with the different .xdo’s. Am I missing something?

Console log items2. Is it an array?

It will matter if the request fails. The format of the response will be different, and therefore any code targetting that response will also fail.

Therefore you will have to add some logic to prevent it trying to map the array if you don’t get a 200 ok response.

My advice is to start with the outerloop. Get that working first, then add the inner loop.

Good news. I removed the innerloop I tried to create and went back to your example and that works fine and as expected:

// step 0 - setup prerequisites
async function asyncCall(request, t) {
    try {
        const response = await pm.sendRequest(request);
        pm.test(t, () => {
            pm.expect(response).to.have.status(200);
        });
        return {
            'data': response,
            'status': "success"
        };
    } catch (error) {
        console.log(error);
        pm.test(t, () => {
            pm.expect(error).to.have.property('code').that.is.undefined;
        });
        return {
            "status": "error",
            "errorcode": error.code
        };
    }
}

pm.test("Status code is 200", function () {
    pm.response.to.have.status(200);
});

const serverPort = pm.environment.get('SERVER_PORT');
const userName = pm.environment.get('USERNAME');
const passWord = pm.environment.get('PASSWORD');
const medewerker = pm.environment.get('MEDEWERKER');
let parseString = require('xml2js').parseString;
const stripPrefix = require('xml2js').processors.stripPrefix;

// step 1 - parse XML\Soap response 
const xmlResponse = pm.response.text();

// step 2 - parse the XML test to JSON - so its easier to work with
parseString(xmlResponse, {
    tagNameProcessors: [stripPrefix],
    ignoreAttrs: true,
    explicitArray: false,
}, async function (err, jsonData) {
    // handle any errors in the XML
    if (err) {
        console.log(err);
    } else {
        // do something with the result
        console.log(jsonData);
        // step 3 - create an array of the item file names
        let items = jsonData.Envelope.Body.getFolderContentsResponse.getFolderContentsReturn.catalogContents.item;
        let fileNameArray = items.map(item => item.fileName);
        console.log(fileNameArray);
        // step 4 - your outerloop
        for (let i = 0; i < fileNameArray.length; i++) {
            console.log(fileNameArray[i]);
            // step 5 - put your sendRequest code here.
            let request2 = {
                url: `http://${serverPort}/xmlpserver/services/v2/CatalogService`,
                method: 'POST',
                header: {
                    'Soapaction': 'http://xmlns.oracle.com/oxp/servce/v2/ReportService/runReportRequest',
                    'Content-Type': 'application/xml',
                    'Accept' : '*/*'
                },
                body: {
                    mode: 'raw',
                    raw: `<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:v2="http://xmlns.oracle.com/oxp/service/v2">
   <soapenv:Header/>
   <soapenv:Body>
      <v2:getFolderContents>
         <v2:folderAbsolutePath>/osi01/Rapporten/${fileNameArray[i]}</v2:folderAbsolutePath>
         <v2:userID>${userName}</v2:userID>
         <v2:password>${passWord}</v2:password>
      </v2:getFolderContents>
   </soapenv:Body>
</soapenv:Envelope>`
                }
            }
            // async sendRequest()
            let request2response = await asyncCall(request2, `Outerloop ${fileNameArray[i]} - status code is 200`);
            if (request2response.status === "success") { // only run final request if previous request is success
            
              // Innerloop
                const innerLoop = ["A", "B", "C"]; // you would get value from response instead
                for (let j = 0; j < innerLoop.length; j++) {
                    let innerloopValue = innerLoop[j];
                    // define POST request
                    const request3 = {
                        url: 'https://postman-echo.com/post',
                        method: 'POST',
                        header: {
                            'Content-Type': 'application/json',
                        },
                        body: {
                            mode: 'raw',
                            raw: JSON.stringify({ 'testValue': innerloopValue })
                        }
                    };
                    // async sendRequest()
                    let request3response = await asyncCall(request3, `Outerloop ${fileNameArray[i]} Innerloop request ${innerloopValue} - status code is 200`);
                    if (request3response.status === "success") {
                       // console.log(request3response.data.json().data.testValue); // Postman Echo Response
                    }
                }
            }
        }
    }
});

I noticed in the console the post that contains the info I need to transfer to the array:

I did some further investigation and discovered the post doesn’t return an array all the time. Sometimes only one item is returned items.map will contain only one value and that’s probably the reason for the previously mentioned error. Is there a way to fix this?

Map can only work against an array or it will error. The resulting map is also an array.

If there is only one item. What does the response look like? You can have one item in an array.

If it doesn’t return an array every time, then you need to have conditional logic to check whether an array is bering returned for items or not. (To stop it causing a fatal error).

If its an array, you run through your loop. If its not an array, then you just run the sendRequest once.

Postman uses JavaScript under the hood, so I’ll let you investigate (aka Google Search) how to check if an object is an array or not.

If you have issues getting it to work, please post your example code and we might be able to help.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.