In REST Assured, what can a FailureConfig failure listener do with a response that then() rejected?
answer
- callback, not a veto
- one method, returns void
- the only handle on a rejected response
- fires just before the throw
- the built-in listener is always appended
basics
~20 sA failure listener receives the request specification, the response specification and the full Response just before the AssertionError is thrown, so it can record or forward them. It cannot change the verdict, edit the message, or retry.
solid answer
~50 s`FailureConfig` holds a list of `ResponseValidationFailureListener`, a single-method functional interface: `void onFailure(RequestSpecification, ResponseSpecification, Response)`. REST Assured fires every registered listener from the response specification immediately before it throws, and again from the `catch` around validation if a matcher itself blows up. That callback is the only way to reach the `Response` on a failing call, because a failed `then()` throws instead of returning something you could `extract()` from — so it is where you snapshot the call-sheet service's error body, its correlation header, and the request that produced them. What it cannot do is change the outcome: `onFailure` returns `void`, the throw happens regardless, and nothing you do there rewrites the assertion message. Register it with `RestAssured.config().failureConfig(failureConfig().with().failureListeners(recorder))`, globally or per request — the call returns a new `FailureConfig`, and the library's own default listener is always appended after yours.
code
java · 19 linesimport io.restassured.listener.ResponseValidationFailureListener;
import static io.restassured.RestAssured.config;
import static io.restassured.RestAssured.given;
import static io.restassured.config.FailureConfig.failureConfig;
ResponseValidationFailureListener recorder = (reqSpec, respSpec, response) ->
evidence.record(response.statusCode(),
response.header("X-Correlation-Id"),
response.asString());
given()
.config(config().failureConfig(failureConfig().with().failureListeners(recorder)))
.contentType("application/json")
.body(duplicateCrewCall)
.when()
.post("/call-sheets/17/crew-calls")
.then()
.statusCode(201);go deeper
Know that REST Assured offers a failure callback at all, and that a failing then() throws rather than handing you a response you could inspect afterwards.
Explain the interface — one void method taking the request spec, the response spec and the Response — and where in validation it fires. Say why it is the only handle on a rejected response.
Show what a listener may safely do at the moment of failure: read the Response, hand it off cheaply, return. Argue why it must never throw, block or attempt a retry from inside the callback.
Decide whether failure capture belongs in a REST Assured listener at all or in the runner's own reporting hook, and own the coupling and per-failure cost that choice creates.
## The interface and where it fires `io.restassured.listener.ResponseValidationFailureListener` is a `@FunctionalInterface` with one method: ```java void onFailure(RequestSpecification requestSpecification, ResponseSpecification responseSpecification, Response response); ``` `FailureConfig` holds a `List` of them. REST Assured's response specification calls every registered listener from a private `fireFailureListeners(response)` step in two places, both inside the same validation pass: - after collecting the failed checks and **immediately before** it throws the `AssertionError` - from the `catch (Throwable)` around validation, when a matcher or a parser blows up rather than simply mismatching, before the original exception is rethrown So a listener runs exactly once, on the call that fails, and always before the failure leaves the library. It does not run on a passing call. ## What a listener can reach The callback exists to solve one specific problem, and the class Javadoc says so outright: a failed `then()` **throws instead of returning anything**, so there is no object left for you to call `extract()` on. The listener is handed the pieces before that happens: - the full `Response` — `statusCode()`, `statusLine()`, `header(name)`, `asString()`, and the body in any form the response side offers - the `RequestSpecification` that produced it - the `ResponseSpecification` that rejected it That is why it is the natural place to snapshot the call-sheet service's error payload and its correlation header at the moment `POST /call-sheets/17/crew-calls` came back 500 instead of 201 — information that the assertion message itself does not carry, because a status mismatch prints only `Expected status code <201> but was <500>.` ## What it cannot do The interface's shape is the answer. `onFailure` returns `void`: - It **cannot change the verdict.** The throw happens whether the listener ran, did nothing, or did a great deal. - It **cannot edit the message.** The `AssertionError` text is built from the failed matchers and the `onFailMessage` string; nothing in the callback contributes to it. - It **cannot retry.** There is no re-send hook here, and REST Assured has no retry facility to reach for anyway. - It **cannot replace the response.** The `Response` is passed for reading; handing back a different one is not part of the contract. - It **must not throw.** If it does, that exception propagates out of `fireFailureListeners` in place of the `AssertionError`, and the mismatch text is lost. Catch inside the listener, always. ## Registration and the listener you cannot remove `FailureConfig` is immutable like every other REST Assured config object. Both `failureListeners(Collection<ResponseValidationFailureListener>)` and `failureListeners(ResponseValidationFailureListener first, ResponseValidationFailureListener... more)` return a **new** `FailureConfig`, so the result has to be assigned back: ```java ResponseValidationFailureListener recorder = (reqSpec, respSpec, resp) -> evidence.record(resp.statusCode(), resp.asString()); RestAssured.config = RestAssured.config() .failureConfig(failureConfig().with().failureListeners(recorder)); ``` The same config can be set globally on `RestAssured.config`, on a request spec builder, or per call with `given().config(...)`. One detail surprises people: the constructor **always appends the library's own default listener** after yours. `getFailureListeners()` therefore never returns just your list — registering one listener leaves two. Your listeners add to the built-in behaviour; they never displace it. The returned config also reports `isUserConfigured() == true`, which is what makes it survive a specification merge. ## When it is the right tool 1. **Evidence you can only get at the moment of failure** — the body, a correlation id, a `Retry-After`-style header — where the assertion message alone would leave a red build undiagnosable. 2. **Routing a failure somewhere** — a report attachment, a metrics counter, a structured record — as a cheap, non-blocking hand-off. 3. **Not** for control flow. If you find yourself wanting the case to continue, the expectation was the wrong shape; fix the assertion, not the listener. Keep the work inside `onFailure` small and total. It runs on the unhappy path, in a build that is already going red, and a listener that blocks on a network call turns one failing case into a slow one. ## The shape of the trade A listener buys you exactly one thing — **reach into the moment of failure** — and charges for it in coupling. Every case that runs under that config now depends on a callback defined somewhere else, and a listener that quietly stops working leaves no trace, because its output is not something any assertion checks. Two habits keep that manageable: - Register it in **one place** — a shared config or a base spec — rather than per case, so there is a single thing to read and a single thing to remove. - Give the listener **no logic of its own**: read fields off the `Response`, hand them to something that knows what to do with them, and return. A listener with branches is a listener that can be wrong on the day you most need it.
- Does registering your own failure listener replace REST Assured's built-in one?No. `FailureConfig`'s constructor always appends its own default listener after whatever you pass, so `getFailureListeners()` returns your listeners plus that one — registering one leaves two. `failureListeners(...)` also returns a new `FailureConfig` rather than mutating the current one, so the result must be assigned back into the config before it takes effect.
- What happens if a failure listener itself throws?The exception propagates out of the listener loop in place of the `AssertionError` REST Assured was about to throw, so the mismatch text is lost and the case reports your listener's failure instead. Keep listeners total: catch inside them, and never let a reporting call, a file handle or a serialization error surface.
A failure listener is the flight recorder, not the pilot: it captures everything about the moment the check failed, and it has no controls that can pull the case out of a red.
saying these in an interview costs you the question
- Thinks a listener can mark the failing case as passed
- Expects onFailure to return a replacement response
- Believes it replaces the library's built-in listener
- Retries the request from inside the callback
- Lets the listener throw and loses the mismatch message
- Assumes it fires on every request, not only on failures