Skip to content

Repeat until and event streams

Two request settings make collection runs work with APIs that don’t answer right away:

  • Repeat until sends a request again and again until a condition holds, for example until a background job reports done.
  • Stream settings say how long a run reads a Server-Sent Events (SSE) request or a GraphQL subscription, so its tests can check the events.

Both only apply in collection runs. A single send in the app ignores them.

Open the request’s Settings tab. Under Repeat until, turn on Repeat in collection runs and fill in:

SettingWhat it meansDefault
ConditionA JavaScript expression checked after each send, after the post-response scripts. Empty: until the request’s tests pass.Empty
EveryMilliseconds to wait between two sends1,000
Give up afterMilliseconds after the first send; then the request fails30,000

The setting is available for HTTP (including GraphQL) and SSE requests. In the request file it’s settings.repeat:

requests/Jobs/Check job.yaml
name: Check job
method: GET
url: "{{base}}/jobs/{{jobId}}"
settings:
repeat:
condition: pm.response.json().status === "done"
intervalMs: 2000
timeoutMs: 60000
scripts:
postResponse: |
pm.test("The job succeeded", () => {
pm.expect(pm.response.json().result).to.equal("ok");
});

For a request with Repeat until on, the runner:

  1. Sends the request, with its pre-request and post-response scripts, like any request of the run.
  2. Checks the condition.
  3. If the condition holds, the request is done: its result is this last send.
  4. If not, and waiting Every milliseconds more would go past Give up after (counted from the first send), the request fails.
  5. Otherwise it waits Every milliseconds and goes back to step 1.

With the defaults (every 1 s, give up after 30 s), a request is sent up to about 30 times. With Give up after set to 0 it’s sent once. The run’s Delay setting is not used between these sends; Every is.

Each send runs all the scripts again, and the variables they set carry over from one send to the next.

The condition is a JavaScript expression with the whole pm API. It holds when it’s truthy.

pm.response.json().status === "done"
pm.response.code === 200 && pm.response.json().items.length > 0

For more than one statement, wrap them in a function that you call right away:

(() => {
const job = pm.response.json();
return job.status === "done" || job.status === "failed";
})()
  • An error thrown while checking (for example pm.response.json() on a body that isn’t JSON yet) counts as “not yet”. The last such error is shown when the request gives up.
  • A syntax error in the condition stops repeating at once and fails the request with a script error.
  • A send that fails (a network error, a timeout, a failing pre-request script) also counts as “not yet”.

Empty condition: the request repeats until its tests pass: every test that isn’t skipped, including tests from workspace and folder scripts and the “Matches the API spec” check. A request without tests repeats until its status is below 400.

The run reports the last send: its status, tests and console. The result also has:

  • The number of sends: 3× in the Runner tab, attempts in the JSON report (present for every request with Repeat until on).
  • The duration from the first send to the end of the last one.

When the condition never held, the request fails with a message such as:

Repeat until: `pm.response.json().status === "done"` didn't hold after 30 sends in 29.1 s
Repeat until: `pm.response.json().status === "done"` didn't hold after 4 sends in 3.0 s (last: SyntaxError: pm.response.json(): the response body is not valid JSON (…))
Repeat until: its tests didn't pass after 12 sends in 11.0 s (last: status 503)

If the last send itself failed (for example with a network error), that error is reported instead.

Two requests in a folder, run in order:

Post-response script of 'Start export'
pm.test("Export accepted", () => pm.response.to.have.status(202));
pm.variables.set("jobId", pm.response.json().id);

Check export calls GET {{base}}/exports/{{jobId}} with Repeat until on, the condition ["done", "failed"].includes(pm.response.json().state), and this test:

Post-response script of 'Check export'
pm.test("Export finished without errors", () => {
pm.expect(pm.response.json().state).to.equal("done");
});

A Server-Sent Events request sends one HTTP request and reads events from the answer as they arrive. In a collection run, Zorvik reads the stream until a stop condition, then runs the request’s post-response scripts with the events.

GraphQL subscriptions run the same way: each result is an event named next, and pm.response.json() is the list of results ([{"data": …}, …]), so a test can check them all:

pm.test("three ticks", () => {
const ticks = pm.response.json().map((r) => r.data.tick);
pm.expect(ticks).to.eql([1, 2, 3]);
});

Open the SSE request’s Settings tab. Under In collection runs:

SettingWhat it meansDefault
Stop at eventStop after the first event with this name. Events without an event: line are named message. Empty: no named event ends the read.Empty
Stop afterStop after this many events. 0: only the time limit (or the server) ends it.100
Time limitMilliseconds from the start of the request (connecting included). At most 300,000 (5 minutes).10,000

Reading stops at the first of these, or when the server closes the stream. In the request file it’s settings.stream:

requests/Jobs/Watch job.yaml
name: Watch job
kind: sse
url: "{{base}}/jobs/{{jobId}}/events"
settings:
stream:
event: done
maxEvents: 100
timeoutMs: 30000

Zorvik adds Accept: text/event-stream and Cache-Control: no-cache unless the request sets these headers.

APIFor an SSE request
pm.response.eventsThe events read, in order: { event, data, id }
pm.response.text()The events as text, in wire format (event:, id: and data: lines, a blank line after each event; the event: line is left out for message events)
pm.response.code, status, headersThe stream’s HTTP response
pm.response.responseTimeMilliseconds from the start of the request until reading stopped
pm.response.responseSizeThe size of the text above

Each event has:

FieldValue
eventThe event name, message when the server sent no event: line
dataThe data, with multiple data: lines joined by \n
idThe last event ID in effect when the event arrived, or null. An id: sent with one event carries over to the following events, as in browsers.
Post-response script of 'Watch job'
const events = pm.response.events;
pm.test("Stream opened", () => pm.response.to.have.status(200));
pm.test("Ends with done", () => {
pm.expect(events).to.not.be.empty;
pm.expect(events[events.length - 1].event).to.equal("done");
});
pm.test("Progress is valid JSON", () => {
events
.filter((e) => e.event === "progress")
.forEach((e) => pm.expect(JSON.parse(e.data)).to.have.property("pct"));
});

The result’s console gets a line saying how the read ended, for example Read 3 events (stopped: the awaited event arrived). The possible reasons:

ReasonMeaning
the awaited event arrivedThe Stop at event event arrived (it’s included in the events)
enough events arrivedStop after events arrived
the time limitTime limit passed
the server ended the streamThe server closed the connection, or reading failed (events read before are kept)
the answer is not an event streamThe status wasn’t 2xx or the Content-Type wasn’t text/event-stream

An SSE request passes and fails by the usual rules: with tests, the tests decide; without tests, a status below 400 passes.

  • Reading ending at the time limit is not a failure by itself. Add a test for the events you expect.
  • When the answer is not an event stream, pm.response.events is an empty array and pm.response.text() holds the start of the answer (up to 4 KB), so a test can show what the server said.
  • When the server doesn’t answer at all within the time limit, the request fails with No answer within N ms.

SSE requests have no Scripts tab in the app. Write the post-response script in the request’s .yaml file (scripts.postResponse, as in the examples above), or put it in the post-response script of a folder that holds the SSE requests.

Repeat until works with SSE requests too, for example to reconnect until a stream delivers an event:

settings:
stream:
event: ready
timeoutMs: 5000
repeat:
condition: pm.response.events.some((e) => e.event === "ready")
intervalMs: 1000
timeoutMs: 60000
LimitValue
Events kept per request1,000 (more are counted but not kept)
Data per event64 K characters (the rest is cut)
Time limit300,000 ms (5 minutes)
Answer kept when it’s not an event streamThe first 4 KB