skip to content

In REST Assured, how does a custom filter change the response before then() validates it?

level: middleimportance: must knowfreq 55%

answer

  1. the response has no setters
  2. clone, then set, then build
  3. your return value is what gets validated
  4. skip clone and you lose the headers

basics

~20 s

Return a new Response built with ResponseBuilder: clone the response that ctx.next handed back, override the body, headers or status code, then build. REST Assured validates whatever your filter returns, so the rebuilt copy is what then() and extract() see.

solid answer

~50 s

`Response` is an interface of readers with no public mutators, so a filter changes the response by returning a different one. The shape is `Response original = ctx.next(requestSpec, responseSpec);` followed by `new ResponseBuilder().clone(original).setBody(masked).build()`. `clone(...)` carries over everything you are not touching — content type, headers, cookies, status code, status line and the parsing configuration that lets `then().body(...)` read the payload — and the setters (`setBody` for `String`, `byte[]` or `InputStream`, plus `setHeader`, `setHeaders`, `setCookies`, `setContentType`, `setStatusCode`, `setStatusLine`) override the parts you want changed. Validation runs on whatever the outermost filter returns, and `extract()` hands out that same object, so on a lighthouse maintenance suite a filter can strip `keeperOnDuty` from `GET /lighthouses/{lighthouseId}` before any assertion or failure log ever sees it. Omitting `clone(...)` is the usual first-attempt bug: the rebuilt response then carries only the fields you set.

code

java · 21 lines
java
import io.restassured.builder.ResponseBuilder;
import io.restassured.filter.Filter;
import io.restassured.response.Response;

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

Filter redactKeeper = (requestSpec, responseSpec, ctx) -> {
    Response original = ctx.next(requestSpec, responseSpec);
    String masked = original.asString()
        .replaceAll("\"keeperOnDuty\"\\s*:\\s*\"[^\"]*\"", "\"keeperOnDuty\":\"REDACTED\"");
    return new ResponseBuilder().clone(original).setBody(masked).build();
};

given()
    .filter(redactKeeper)
.when()
    .get("/lighthouses/pigeon-point")
.then()
    .statusCode(200)
    .body("keeperOnDuty", equalTo("REDACTED"));

go deeper

for a junior

Remember the three-step shape: take the response from ctx.next, clone it into a ResponseBuilder, set what you are changing and build. Knowing that the response itself has no setters is most of the answer.

for a middle

Explain what clone carries over and what a bare builder loses, and be able to say that validation runs on the filter's return value, which is why the rebuilt object is the one the test grades.

for a senior

Talk about restraint. Rebuilding is right for redaction or unwrapping a house envelope and wrong for patching a payload into passing, because the printed response and the served response then disagree during a real incident.

for a principal

Frame it as a trust question for the suite. Any body rewriting between the service and the assertion weakens the evidence a run produces, so decide where that is allowed and make it visible in the failure output.

## The response you were handed cannot be edited `io.restassured.response.Response` is an interface of readers — `asString()`, `asByteArray()`, `getStatusCode()`, `getStatusLine()`, `getHeaders()`, `getDetailedCookies()`, `jsonPath()` — with no public mutators anywhere on it. So "changing the response" from inside a filter never means setting a field on the object `ctx.next(requestSpec, responseSpec)` returned. It means producing a **different** `Response` and returning that one instead, and `io.restassured.builder.ResponseBuilder` is the class that exists for exactly this job. That design is deliberate rather than an oversight. A filter is around advice: the response object it receives is the value flowing back out of the chain, and letting one link quietly mutate a shared object would make the value seen by the next link depend on execution order in a way nobody could read from the call site. Returning a new object keeps the flow explicit — whatever you return **is** the response from this point outwards, and whatever you do not return is discarded. ## The clone, set, build shape ```java Response original = ctx.next(requestSpec, responseSpec); String masked = redactKeeper(original.asString()); return new ResponseBuilder().clone(original).setBody(masked).build(); ``` - `clone(original)` copies everything you are *not* changing: the content, content type, headers, cookies, status code and status line, plus internal wiring — the `RestAssuredConfig`, the decoder config, the session-id name and the response-parser registry — that lets `then().body(...)` parse the payload at all. - `setBody(...)` takes a `String`, a `byte[]` or an `InputStream`, and each overload rejects `null` with an `IllegalArgumentException` rather than quietly emptying the response. - `setHeaders(Headers)`, `setHeader(name, value)`, `setCookies(Cookies)`, `setContentType(String|ContentType)`, `setStatusCode(int)` and `setStatusLine(String)` cover the rest of the surface. - `build()` produces the `Response` you return from `filter(...)`. - The builder is a plain object with no link back to the chain, so it is safe to construct one per invocation; do not hold one in a field of the filter and reuse it across requests. Skipping `clone(...)` is the classic first-attempt bug. A bare `new ResponseBuilder().setBody(json).build()` returns a response carrying **only** what you set — no headers, no status line, none of the original's parsing configuration — so assertions that passed a minute ago start failing for reasons that have nothing to do with the lighthouse service. ## Where the rebuilt response lands Validation runs after the whole chain has unwound. `then().statusCode(200).body("lampStatus", equalTo("LIT"))` is applied to whatever the outermost filter returned, and `extract()` hands out that same object. To the test, a rebuilt response is indistinguishable from one the service sent. | you want to change | what you call | what `clone(...)` still carries | |---|---|---| | the payload | `setBody(String\|byte[]\|InputStream)` | status code, status line, headers, cookies | | a header | `setHeader(name, value)` or `setHeaders(Headers)` | body, status, cookies, parsing config | | the status | `setStatusCode(int)`, `setStatusLine(String)` | body, headers, cookies, parsing config | | nothing (short-circuit) | build a response and never call `ctx.next(...)` | nothing — you own every field | That last row is a different operation from the other three. Rebuilding happens **after** `ctx.next(...)` and edits a real answer from the service; short-circuiting happens **instead of** `ctx.next(...)` and means no request was ever made. ## What the rest of the chain sees Filters nest, so a rebuild is not private to the filter that performs it. The response your filter returns is the value its own caller receives from *its* `ctx.next(...)` call, which means every filter that ran earlier — and therefore sits outside yours — sees the rebuilt object rather than the original one, and may rebuild it again on the way out. Three consequences are worth holding on to: - A logging filter placed outside a rewriting filter prints the rewritten payload, not what arrived. - A rewriting filter placed outside another rewriting filter operates on the inner one's output. - Nothing anywhere keeps a copy of the untouched response, so if the original body matters for diagnosis you have to capture it inside the filter that first receives it. ## A worked lighthouse case `GET /lighthouses/{lighthouseId}` on the maintenance API returns `keeperOnDuty` with a real person's name in it, and the suite prints response bodies on failure. A filter that calls `ctx.next(...)`, rewrites `keeperOnDuty` to `REDACTED`, and returns `new ResponseBuilder().clone(original).setBody(masked).build()` keeps the name out of every artefact downstream of the filter, while `lighthouseId`, `lampStatus` and the 200 survive untouched because `clone(...)` carried them. ## When not to reach for it Rebuilding a response is a sharp tool, and the reasons to refuse it are worth saying out loud in a review: 1. **Never rebuild to make an assertion pass.** A filter that patches a missing field turns a real contract defect into a green run, and the next reader has no way to see it from the test. 2. **Never rebuild what the request could have avoided.** If the payload is wrong because the call was wrong, fix the call. 3. **Say so in the failure output.** A rewritten body that nobody expects is the hardest kind of test to debug, because the printed response and the served response differ. 4. **Keep the rewrite narrow.** Redaction, decompression of a bespoke envelope and normalising a known quirk are defensible; general body munging is not.

  • What breaks if you build a response in a filter without calling clone(...) first?
    You return a response that carries only the fields you set. The headers, cookies, status line and the original's parsing configuration are all gone, so assertions such as `then().contentType(ContentType.JSON)` or a `body(...)` path expression start failing for reasons unrelated to the service. `clone(original)` copies those parts across first, which is why it is the opening call in almost every rebuild.
  • Does the response a filter rebuilds also reach extract(), or only then()?
    Both. The value the outermost filter returns is the single `Response` the DSL carries forward, so `then()` validates it and `extract().path(...)`, `extract().jsonPath()` and `extract().response()` all hand back the rebuilt object. There is no second copy of the original response kept anywhere for the test to reach.

A response inside a filter is a sealed photograph, not a whiteboard: you cannot write on the one you were handed. ResponseBuilder.clone makes a duplicate you may mark up, and the duplicate is the one the test grades.

saying these in an interview costs you the question

  • Tries to mutate the Response object returned by ctx.next
  • Builds a new response without cloning the original first
  • Thinks the original response still reaches then() after a rebuild
  • Rebuilds a response to make a failing assertion go green
  • Confuses rebuilding after ctx.next with short-circuiting instead of it