skip to content

In REST Assured, how do RestAssured.filters(...) and replaceFiltersWith(...) differ?

level: middleimportance: nice to knowfreq 33%

answer

  1. the list is private, not a field
  2. one name adds, one name clears
  3. the plural name is not a setter
  4. nothing de-duplicates the list
  5. noFiltersOfType opts one request out

basics

~20 s

Both register default filters applied to every subsequent request, but RestAssured.filters(...) appends to the existing private static list, while replaceFiltersWith(...) clears the list first and then adds. The no-argument filters() getter returns an unmodifiable view of it.

solid answer

~40 s

REST Assured keeps its default filters in a `private static List<Filter>` on the `RestAssured` class, so you never assign it — you call methods. `RestAssured.filters(List<Filter>)` and `RestAssured.filters(Filter, Filter...)` **append**: the list keeps whatever was in it. `RestAssured.replaceFiltersWith(List<Filter>)` and `replaceFiltersWith(Filter, Filter...)` call `clear()` first, then append, so the list ends up holding exactly what you passed. The no-argument `RestAssured.filters()` is the getter and hands back `Collections.unmodifiableList(...)`. Nothing de-duplicates, so calling `filters(new ResponseLoggingFilter())` in two setup paths logs every bell-tower response twice. Whatever is in the list at the moment `given()` runs is copied into that request's specification, so the list is a template read per request rather than a live reference, and `RestAssured.reset()` empties it outright. For a single call you opt out with `given().noFilters()` or `given().noFiltersOfType(ResponseLoggingFilter.class)` instead of un-registering a global.

code

java · 17 lines
java
import io.restassured.RestAssured;
import io.restassured.filter.log.ErrorLoggingFilter;
import io.restassured.filter.time.TimingFilter;

RestAssured.replaceFiltersWith(new ErrorLoggingFilter());
RestAssured.filters().size();          // 1 - clear() then add

RestAssured.filters(new TimingFilter());
RestAssured.filters().size();          // 2 - appended, not replaced

RestAssured.filters(new TimingFilter());
RestAssured.filters().size();          // 3 - nothing de-duplicates

RestAssured.replaceFiltersWith(new ErrorLoggingFilter());
RestAssured.filters().size();          // 1 - back to exactly one

RestAssured.filters().clear();         // UnsupportedOperationException

go deeper

for a junior

Know that REST Assured can hold default filters that apply to every request, and that you register them with RestAssured.filters(...) or RestAssured.replaceFiltersWith(...) rather than by assigning a field.

for a middle

Explain the one behavioural difference: filters(...) appends and replaceFiltersWith(...) clears first. Add that nothing de-duplicates, and that the no-argument filters() returns an unmodifiable view.

for a senior

Show how you keep this safe in a real suite: register once at startup, assert on filters().size() in teardown, and use noFilters() or noFiltersOfType(...) for the one request that needs to differ.

for a principal

Weigh a global filter against a filter carried on a specification each suite owns. The global is one line and invisible to every later reader; the specification is explicit and survives a suite that grows past one team.

REST Assured's default filters sit slightly apart from the rest of its static entry surface. `baseURI`, `port`, `basePath` and the others are `public static` fields you assign. The filter list is not: it is declared `private static List<Filter> filters = new LinkedList<Filter>()`, and the only way to change it is through methods on `RestAssured`. That difference is deliberate, and the two method names differ in exactly one behaviour that suites get wrong. ## Append versus replace | Call | Effect on the static list | |---|---| | `RestAssured.filters(List<Filter>)` | `addAll(...)` — existing entries survive | | `RestAssured.filters(Filter, Filter...)` | `add(...)` then `addAll(...)` — existing entries survive | | `RestAssured.replaceFiltersWith(List<Filter>)` | `clear()`, then append | | `RestAssured.replaceFiltersWith(Filter, Filter...)` | `clear()`, then append | | `RestAssured.filters()` (no arguments) | reads it back as `Collections.unmodifiableList(...)` | | `RestAssured.reset()` | swaps in a new empty `LinkedList` | The plural name `filters(...)` reads like a setter and is not one. That single misreading produces two opposite bugs: - A suite that calls `filters(...)` twice — once in a base class, once in a subclass — ends up with the filter registered twice and nothing complains, because nothing de-duplicates the list. - A suite that meant to *add* a timing filter and reached for `replaceFiltersWith(...)` silently deletes the logging filter its CI output depended on. ## What a registered default filter actually does When `RestAssured.given()` builds a request, it constructs a fresh `RequestSpecificationImpl` and the constructor does `this.filters.addAll(filters)` from the static list. So the static list is a **template that is copied per request**, not a live reference the request keeps watching. A filter you register after a chain has started building is not in that chain, and a filter registered globally is in every chain built afterwards, in every test class, for the rest of the JVM's life. That last clause is the reason this belongs to the global-defaults topic rather than to filters as a subject. A globally registered filter is one of the hardest kinds of leaked static state to see, because it produces no visible setting anywhere — just extra behaviour on requests that never asked for it. ## Reading the list back `RestAssured.filters()` with no arguments is genuinely useful in a bell-tower roster suite: ```java RestAssured.replaceFiltersWith(new ErrorLoggingFilter()); // ... a whole test class runs ... assertEquals(1, RestAssured.filters().size()); // nothing was smuggled in ``` Because the returned list is unmodifiable, a test cannot accidentally mutate the library's state while inspecting it — an `UnsupportedOperationException` is thrown instead of a silent change. Two habits pay for themselves: 1. Assert on `RestAssured.filters().size()` in a suite-level teardown to catch a class that registered a filter and forgot. 2. Log `RestAssured.filters()` at the start of a run so the CI output records which filters were actually active. ## Opting out for a single request A globally registered filter is not a life sentence for one call. The `RequestSpecification` carries two escape hatches: - `given().noFilters()` drops every filter for that one request. - `given().noFiltersOfType(ResponseLoggingFilter.class)` drops only the ones assignable to the given type. For a bell-tower roster suite this is how you keep a global `ResponseLoggingFilter` while silencing the one endpoint that returns a 4 MB peal archive: ```java given() .noFiltersOfType(ResponseLoggingFilter.class) .when() .get("/towers/st-cuthberts/peals?year=1892") .then() .statusCode(200); ``` ## Where the registration belongs The static list is process-wide, so *where* you call these methods matters more than which one you pick: - One suite-level startup point is the only safe home. It runs once, and `replaceFiltersWith(...)` there is idempotent even if the run somehow reaches it twice. - A base class that every test class extends is the classic trap: its setup runs again for each subclass, so `filters(...)` there grows the list once per class and duplicates every filter. - A test method is never the right place. Whatever it registers survives the method, the class and the rest of the run. - If any class calls `RestAssured.reset()`, the registration point has to run again afterwards, because `reset()` swapped in an empty list. ## The practical rules - Use `replaceFiltersWith(...)` in the one place your suite configures itself; it is idempotent, so re-running it is safe. - Use `filters(...)` only when you genuinely mean *add to whatever is already there*, and know what that is. - Never call either one from a test method. A filter registered mid-run applies to every request after it, in every class the runner reaches next. - Remember that `reset()` empties the list, so a teardown that resets must re-register the suite's filters or the rest of the run has none. - Reach for `noFilters()` or `noFiltersOfType(...)` on the request, rather than un-registering and re-registering a global, whenever one call needs to be different.

  • Why does REST Assured return an unmodifiable list from the no-argument filters()?
    So inspecting the default filters cannot become a way of changing them. `Collections.unmodifiableList(...)` throws `UnsupportedOperationException` on `add`, `remove` or `clear` instead of quietly mutating process-wide state. Every intended change has to go through `filters(...)`, `replaceFiltersWith(...)` or `reset()`, which keeps the append-versus-replace decision explicit at the call site.
  • A globally registered ResponseLoggingFilter floods one endpoint's output. What do you change?
    Nothing global. Put `given().noFiltersOfType(ResponseLoggingFilter.class)` on that one request, or `given().noFilters()` if you want every filter off for it. Un-registering the global and re-registering it afterwards would leave the rest of the run unfiltered if the test failed in between, and would make the suite order dependent.

saying these in an interview costs you the question

  • Reads filters(...) as a setter that replaces the list
  • Thinks RestAssured.filters is a public field you assign
  • Assumes REST Assured de-duplicates repeated filter registrations
  • Calls replaceFiltersWith(...) inside a test method
  • Tries to mutate the list returned by the no-argument filters()