skip to content

In REST Assured, what must a class implementing the Filter interface do for the request to be sent?

level: juniorimportance: should knowfreq 52%

answer

  1. one method, three arguments
  2. around advice, not two callbacks
  3. before next is request, after is response
  4. forget the chain call, nothing sends

basics

~20 s

Implement the single filter method that takes a FilterableRequestSpecification, a FilterableResponseSpecification and a FilterContext, then return ctx.next on those two specifications. That chain call is what reaches REST Assured's internal sending filter; without it no request ever leaves the test.

solid answer

~40 s

`io.restassured.filter.Filter` is a functional interface with a single method: `Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec, FilterContext ctx)`. One method sees both halves of the call. Whatever you do before `ctx.next(requestSpec, responseSpec)` shapes the outgoing request — on a lighthouse maintenance suite, stamping `X-Correlation-Id` onto `POST /lighthouses/{lighthouseId}/maintenance-visits` — and whatever you do after it works on the `Response` that came back. The chain call is the half everyone forgets on the first attempt. REST Assured appends an internal `SendRequestFilter` to the end of the list, so `ctx.next(...)` is the only route to the code that opens the connection: omit it and your method returns `null`, no request goes out, and the test dies on a null response rather than on an assertion. Register with `given().filter(myFilter)`, or inline a lambda for one-off work.

code

java · 18 lines
java
import io.restassured.filter.Filter;

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

Filter correlationFilter = (requestSpec, responseSpec, ctx) -> {
    requestSpec.replaceHeader("X-Correlation-Id", "maintenance-run-4711");
    return ctx.next(requestSpec, responseSpec);
};

given()
    .filter(correlationFilter)
    .pathParam("lighthouseId", "pigeon-point")
.when()
    .get("/lighthouses/{lighthouseId}/lamp")
.then()
    .statusCode(200)
    .body("lampStatus", equalTo("LIT"));

go deeper

for a junior

Be ready to write the method signature from memory and to say out loud that the last line returns ctx.next on the two specifications. Interviewers ask for the shape, not for a clever filter.

for a middle

Explain the mechanics: the filter list is sorted and an internal send filter is appended last, so ctx.next is the only path to the code that opens the connection. Say which edits belong before that call.

for a senior

Show judgment about what belongs in a filter at all. Cross-cutting concerns such as correlation ids and redaction fit; per-test logic does not, and a stateful filter instance shared across a parallel suite is a defect waiting to happen.

for a principal

Own the question of how much behaviour a suite hides in filters. Every filter is invisible at the call site, so argue for a small, documented set with clear ownership rather than a growing pile that nobody can reason about.

## One method, both directions `io.restassured.filter.Filter` lives in the `rest-assured` artifact, is annotated `@FunctionalInterface`, and declares exactly one method: ```java Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec, FilterContext ctx); ``` That is the whole contract. There is no `before`/`after` pair, no `onResponse` callback and nothing to register beyond handing the instance to `given().filter(...)`. The library's own Javadoc calls a filter **around advice**, and that is the mental model that keeps you out of trouble: your method is entered on the way out to the service and returns on the way back, so the outgoing request and the incoming response are both visible inside one stack frame. Because it is a functional interface, a throwaway filter can be a lambda and a reusable one a named class. ## What each argument gives you - `requestSpec` is a `FilterableRequestSpecification`. It extends `RequestSpecification` **and** `QueryableRequestSpecification`, so you can both read the call (`getURI()`, `getMethod()`, `getHeaders()`, `getBody()`) and change it with the ordinary DSL methods. - It adds mutators the normal DSL has no use for: `removeHeader`, `replaceHeader`, `removeCookie`, `replaceCookie`, `removeQueryParam`, `removeFormParam`, `removePathParam` and `path(String)`. - `responseSpec` is a `FilterableResponseSpecification` — a read-only view of the expectations the test already declared: `getStatusCode()`, `getStatusLine()`, `getResponseContentType()`, `getRootPath()`, `getLogDetail()`, `hasHeaderAssertions()`. - `ctx` is the `FilterContext`: `next(request, response)` to continue the chain, `send(requestSender)` to fire an extra request at the same method and URI, and `setValue`/`getValue`/`hasValue` to leave a value for the filters behind you. ## Why the chain call is not optional Before a request goes out, REST Assured takes the filter list for that request, sorts it, and appends an **internal `SendRequestFilter`** as the last element. That internal filter is the code that opens the connection. Your filter is not called by a dispatcher that will send the request afterwards — your filter is one link in a chain, and `ctx.next(requestSpec, responseSpec)` is the only route to the next link. | what your method returns | what actually happens | |---|---| | `ctx.next(requestSpec, responseSpec)` | the rest of the chain runs, the request is sent, you get the real `Response` | | `null`, or nothing at all | nothing is sent; the DSL is handed `null` and the test dies on a null response, not on an assertion | | a `Response` you built yourself | nothing is sent; your object is validated as if the service had returned it | The third row is the one legitimate reason to skip the call: a filter that returns a `Response` built with `ResponseBuilder` short-circuits the lighthouse call inside the test process. Every other job a filter does — stamping headers, logging, correlating, timing — must return the chain result. ## Rewriting the lighthouse request before it leaves Order *inside* the method matters as much as order between filters. Everything above the `ctx.next(...)` line is still "before the request"; everything below it runs after the response has already come back. A filter that stamps `X-Keeper-Token` onto `POST /lighthouses/{lighthouseId}/maintenance-visits` *after* calling `ctx.next(...)` compiles, runs, mutates the specification object and changes nothing on the wire, because the request has already gone. - Edit the request **before** `ctx.next(...)`, every time. - Prefer `replaceHeader("X-Keeper-Token", token)` over `header(...)` when a value may already be present, or the request carries the header twice. - `ctx.next(...)` rebuilds the filter context from the request object you pass it, so a path or query change made just above the call is picked up by the rest of the chain. - The context holds an iterator over the remaining filters and consumes it, so calling `ctx.next(...)` twice does not replay the chain — it continues past the filter the first call already ran. ## Registering it and living with it `given().filter(new KeeperTokenFilter())` adds a filter to one request and `given().filters(...)` adds several; `noFilters()` clears the list for this request and `noFiltersOfType(Class)` drops one kind. Two habits keep hand-written filters cheap to own: 1. Keep the instance stateless unless the state is the point — a filter object registered on a shared specification is reused by every request that specification builds. 2. Make the filter do one thing. A filter that stamps a keeper token *and* redacts a response body is two reasons to change one `filter(...)` method, and it is the filter that always breaks last.

  • What does a filter gain by implementing io.restassured.spi.AuthFilter instead of plain Filter?
    `AuthFilter` is a marker interface that extends `Filter` and adds no methods, but REST Assured treats it specially: when a request calls `given().auth().none()`, every filter implementing `AuthFilter` is removed from that request's chain. A hand-written credential filter therefore disappears together with the scheme it stands in for, while a plain `Filter` survives `auth().none()` and keeps stamping.
  • Can a filter call ctx.next(...) twice to re-run the chain?
    No. The `FilterContext` holds an iterator over the remaining filters and `next(...)` consumes it, so a second call continues from where the first stopped instead of replaying the chain, and returns `null` once the iterator is spent. REST Assured has no built-in retry; repeating a call means repeating the `given()` chain from outside the filter.

saying these in an interview costs you the question

  • Expects the interface to have separate before and after methods
  • Returns null from the filter method instead of the chain result
  • Assumes REST Assured sends the request whether or not ctx.next is called
  • Edits the request specification after calling ctx.next
  • Treats ctx.send as the normal way to continue the chain