skip to content

Retrying Until Ready

What REST Assured gives you when an endpoint answers before it is finished: no retry, no polling and no timeout method - only a measured round trip and the HTTP client's own settings.

part ofREST Assuredoverview, primer and where to startread it →
on this pageshow

questions

5

In REST Assured, how do you wait for a tram fare-inspection to become SETTLED before asserting?

level: middleimportance: must knowfreq 62%

answer

  1. a request builder, not a waiting library
  2. no retry, no poll, no timeout
  3. you supply the loop, it supplies the call
  4. extract inside the loop, assert outside

basics

~20 s

REST Assured has no retry, no polling and no timeout method on its request DSL. You write the waiting yourself, in a bounded loop or a library such as Awaitility, and REST Assured supplies each call and the final assertion.

solid answer

~40 s

REST Assured is a request builder and an assertion library, not a waiting library. `RequestSpecification` declares no `retry`, no `pollUntil` and no `timeout`, and the core module's main sources contain no retry or polling code at all. So a tram fare-inspection that answers `202 Accepted` and only later flips `status` from `PENDING` to `SETTLED` has to be waited on by code you write: a hand-written loop with a deadline, a waiting library such as Awaitility, or a retry the runner applies to the whole method. Inside that loop you call `given().spec(fares).when().get("/inspections/{inspectionId}")` and read the current value with `extract().path("status")`. Keep the real expectation out of the loop and make one validating call afterwards, so `then().body("status", equalTo("SETTLED"))` is what produces the failure message. REST Assured contributes the round trip and the assertion; the waiting is yours.

code

java · 23 lines
java
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.specification.RequestSpecification;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.equalTo;

RequestSpecification fares = new RequestSpecBuilder()
        .setBaseUri("https://fares.tramnet.test")
        .build();

String status = "PENDING";
long deadline = System.currentTimeMillis() + 20_000L;
while ("PENDING".equals(status) && System.currentTimeMillis() < deadline) {
    Thread.sleep(500L);
    status = given().spec(fares).pathParam("inspectionId", inspectionId)
            .when().get("/inspections/{inspectionId}")
            .then().statusCode(200)
            .extract().path("status");
}

given().spec(fares).pathParam("inspectionId", inspectionId)
        .when().get("/inspections/{inspectionId}")
        .then().body("status", equalTo("SETTLED"));

go deeper

for a junior

Be ready to say plainly that a REST Assured call sends one request and never repeats it on its own. Naming the loop as your responsibility rather than the library's is most of the answer at this level.

for a middle

Explain the split: the library owns request building, sending and assertion, while the repetition, the interval and the deadline live in code around it. Name at least two ways to supply that loop.

for a senior

Show the loop you would actually ship — a bounded read that extracts a value, then one validating call afterwards — and say what the failure message looks like in each shape.

for a principal

Own the consequence for the suite: a library with no waiting primitive means every team invents one. Decide whether waiting is a shared harness concern or left to each test, and defend that choice.

## What REST Assured declines to do REST Assured is two things: a builder for one HTTP request and a validator for the one response that comes back. Its request surface is `io.restassured.specification.RequestSpecification` and its validation surface is `io.restassured.response.ValidatableResponseOptions`. Neither declares a `retry`, a `pollUntil`, an `eventually` or a `timeout`. That is not a gap you can chain your way around, because the behaviour is absent from the implementation as well as from the interface: the core module's main sources contain no occurrence of `retry`, `poll` or `await` at all, and `timeout` survives there only inside a Javadoc `@param` on `ResponseSpecBuilder.expectResponseTime`. There is exactly one real `timeout(...)` anywhere in the project, and it is somewhere else entirely: the `spring-mock-mvc` module's `AsyncConfig` and `MockMvcRequestAsyncConfigurer.timeout(...)`, which bound Spring's own asynchronous dispatch inside a mocked servlet container. No socket is involved, and it does nothing for you against a deployed service. ## The problem the library leaves you A tram fare-inspection API is a normal example of the shape. `POST /inspections` answers `202 Accepted` with an `inspectionId` and a `Location` header. `GET /inspections/{inspectionId}` then reports `"status": "PENDING"` while the fare engine matches the tap-in record, and only later flips it to `SETTLED` with a `penaltyNoticeId` attached. One REST Assured call reads whichever of those moments it happened to hit. Reading it again is your code's decision, not the library's. Three places the repetition can live: - **A hand-written loop**, in the test or in a helper: a `while` with a deadline wrapped around `given()...when().get(...)`. No new dependency, and you own every part of the correctness. - **A waiting library** such as Awaitility, which supplies the loop, the interval and the deadline and takes your condition as a lambda. The REST Assured call sits inside that lambda unchanged. - **A retry applied by the test runner** to the whole method. Cheap to switch on, but it re-runs setup and every assertion rather than just the wait, and where a retry belongs in a suite is a framework-architecture decision rather than a REST Assured one. ## What REST Assured does contribute | Concern | Who owns it | |---|---| | Building and sending each attempt | REST Assured — `given()...when().get(...)` | | Reading the current value | REST Assured — `extract().path("status")` | | Deciding to try again, and when to stop | your loop, or a waiting library | | The final expectation and its failure message | REST Assured — `then().body(...)` | | Bounding one attempt in time | the Apache HTTP client, through `HttpClientConfig` | Two mechanics make the loop cheap to write correctly: 1. `RestAssured.given()` builds a fresh specification on every call, so each attempt is a clean request rather than an accumulation of the previous one. Shared setup belongs in a `RequestSpecBuilder`, built once and attached with `given().spec(fares)`. 2. `extract().path("status")` hands the value back as an ordinary Java object. A loop condition written against that is plain Java, and it cannot throw on an attempt that is simply not ready yet. ## The shape that fails review The tempting loop puts the real expectation inside it and catches the failure: ```java while (!settled) { try { given().spec(fares).pathParam("inspectionId", inspectionId) .when().get("/inspections/{inspectionId}") .then().body("status", equalTo("SETTLED")); settled = true; } catch (AssertionError stillPending) { Thread.sleep(500L); } } ``` REST Assured signals every failed expectation the same way — an `AssertionError` thrown from its internal response specification — so that catch cannot distinguish "still `PENDING`" from "the endpoint returned 500", "the field was renamed to `settlementStatus`" or "the host is unreachable". Each of those becomes a slow timeout whose message names your deadline instead of the defect. The shape that survives review inverts it: - Inside the loop, validate only invariants that hold on every attempt. `then().statusCode(200)` on the status resource is fine, because a pending inspection still answers 200. - Inside the loop, read the changing value with `extract()` and compare it in plain Java. - After the loop, make one ordinary validating call, so `then().body("status", equalTo("SETTLED"))` produces the mismatch message the report will carry. ## What not to confuse it with - `then().time(matcher)` measures a round trip that has already finished. REST Assured appends a `TimingFilter` automatically when you have not registered one, so the number is always available — but measuring a call is not bounding it, and it is certainly not waiting for one. - A read timeout set through `HttpClientConfig` bounds a single attempt. Fifty bounded attempts still need a loop to make them fifty. - The silence is deliberate rather than missing. Which signal means "ready", how long to wait and how often to ask are test-design questions, and REST Assured takes no position on any of them — which is precisely why the answer to "how do I wait" is always "in code you wrote".

  • Which REST Assured call inside the loop gives you the current status as a plain Java value?
    `extract().path("status")`, on the extract side of the DSL after `then()`. It returns the value as an ordinary object you can compare in a `while` condition, so the loop never depends on a matcher that could throw. `extract().jsonPath()` gives you the whole document if an attempt needs several fields. The validating expectations stay outside the loop.
  • Does the spring-mock-mvc module's timeout(...) help you wait for a deployed tram fare-inspection API?
    No. `AsyncConfig` and `MockMvcRequestAsyncConfigurer.timeout(...)` live in the `spring-mock-mvc` module and bound Spring's own asynchronous dispatch inside a mocked servlet container, with no socket involved. Against a deployed service you are using core REST Assured, which has no `timeout(...)` at all, so the wait is still a loop you write.

REST Assured is a camera, not a stakeout. It takes one sharp picture of the moment you asked for and hands it back; deciding to come back in half a second and take another is entirely your job.

saying these in an interview costs you the question

  • Claims REST Assured has a retry() or pollUntil() method on the request DSL
  • Thinks then().time(lessThan(2000L)) aborts a slow request
  • Wraps every call in catch (AssertionError) to keep polling
  • Assumes given() carries state so the next call counts as a retry
  • Says a longer socket timeout is the same thing as waiting for readiness
open as a page

Why should a REST Assured polling loop not use then().statusCode(200) as its loop condition?

level: seniorimportance: must knowfreq 47%

basics

~20 s

Because REST Assured validation is not a boolean: a failed expectation throws AssertionError. Using it as a loop condition means catching AssertionError every iteration, which silently swallows real failures as not-ready-yet and ends with a timeout message naming nothing.

open as a page

In REST Assured, where do you set a connect or read timeout, given the DSL has no timeout() method?

level: seniorimportance: must knowfreq 54%

basics

~20 s

Timeouts are the Apache HTTP client's, not REST Assured's. Reach them through HttpClientConfig: setParam puts a parameter on the client REST Assured builds, and httpClientFactory replaces that client so you configure it yourself. There is no DSL timeout method.

open as a page

In REST Assured, how do httpClientConfig().setParam(...) and httpClientFactory(...) differ for timeouts?

level: seniorimportance: should knowfreq 39%

basics

~20 s

REST Assured's setParam stores an entry it writes onto the parameters of the client it already built, so the client must accept them. httpClientFactory replaces construction, letting you configure timeouts inside your own factory before REST Assured sees it.

open as a page

REST Assured ships no retry or timeout DSL - how would you wire waiting across a large suite?

level: principalimportance: should knowfreq 35%

basics

~20 s

REST Assured leaves two gaps and a suite should fill them separately: a per-attempt time bound carried by one shared RestAssuredConfig, and a readiness wait in one helper that extracts values while the real expectations stay outside it.

open as a page