In REST Assured, which status codes trigger ErrorLoggingFilter, and how does that differ from then().log().ifError()?
answer
- two predicates, one upper bound
- allOf on the filter, plain compare on the DSL
- 500 inclusive, 503 outside
- ResponseLoggingFilter takes any Matcher<Integer>
basics
~20 sREST Assured's ErrorLoggingFilter fires only on status codes 400 through 500 inclusive, because it is built with Hamcrest allOf(greaterThanOrEqualTo(400), lessThanOrEqualTo(500)). The then().log().ifError() shortcut uses a statusCode >= 400 test instead. So a 503 prints through ifError() but never through ErrorLoggingFilter.
solid answer
~50 sThey look like the same predicate and are not. `ErrorLoggingFilter` calls its superclass with `allOf(greaterThanOrEqualTo(400), lessThanOrEqualTo(500))`, so its window is 400 to 500 **inclusive** -- a 500 triggers it, but a 501, 502, 503 or 504 does not. REST Assured's `then().log().ifError()` is a plain `statusCode >= 400` check with no upper bound, so every 5xx prints. On a museum ticketing API where `POST /v1/tickets` returns 503 whenever the payment gateway is unreachable, a suite relying on `ErrorLoggingFilter` alone gets a red test with an empty log, which is the classic wasted CI cycle. If you want the filter form with full 5xx coverage, register `new ResponseLoggingFilter(greaterThanOrEqualTo(400))` instead, which takes any `Matcher<Integer>`. Neither one prints the request, either -- both show the response only, so request logging stays a separate decision you make on its own.
code
java · 32 linesimport io.restassured.filter.log.ErrorLoggingFilter;
import io.restassured.filter.log.ResponseLoggingFilter;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
public class GatewayFailureLogging {
// 503 from the payment gateway: this prints NOTHING
public void silentOn503() {
given()
.baseUri("https://api.museum.example")
.filter(new ErrorLoggingFilter())
.contentType("application/json")
.body("{\"exhibitionId\":\"pompeii\",\"slotId\":\"2026-04-11T10:00\"}")
.post("/v1/tickets")
.then()
.statusCode(201);
}
// same call, same failure: this prints the 503 response in full
public void loudOn503() {
given()
.baseUri("https://api.museum.example")
.filter(new ResponseLoggingFilter(greaterThanOrEqualTo(400)))
.contentType("application/json")
.body("{\"exhibitionId\":\"pompeii\",\"slotId\":\"2026-04-11T10:00\"}")
.post("/v1/tickets")
.then()
.statusCode(201);
}
}go deeper
Know that ErrorLoggingFilter exists and prints a failing response for you. If you remember only one number, remember that its range stops at 500 and does not reach 503.
Be able to quote both predicates and say which codes differ. Explain that the filter is built with a Hamcrest allOf and the response-side shortcut is a plain comparison, and name ResponseLoggingFilter with a matcher as the fix.
Diagnose the symptom rather than the class: a red build with an empty log on a gateway failure. Standardise the suite on one predicate, and say why you would not let two mechanisms with different ranges coexist.
Decide the team's default failure-evidence policy and where it lives -- a shared specification, not per test. Weigh log volume against diagnosability, since printing every 4xx in a large suite can bury the one failure that mattered.
This is a false sentence made entirely of true tokens: *"`ErrorLoggingFilter` and `then().log().ifError()` both log errors."* Both statements are individually defensible and the conclusion -- that they behave the same -- is wrong. The difference is one Hamcrest matcher, and it decides whether a gateway failure ever reaches your build log. ## The two predicates, side by side | | Predicate | 404 | 500 | 503 | | --- | --- | --- | --- | --- | | `ErrorLoggingFilter` | `allOf(greaterThanOrEqualTo(400), lessThanOrEqualTo(500))` | prints | prints | **silent** | | `then().log().ifError()` | `statusCode >= 400` | prints | prints | prints | `ErrorLoggingFilter`'s entire body is two constructors and two static factories. The working constructor is `super(stream, allOf(greaterThanOrEqualTo(400), lessThanOrEqualTo(500)))`, which hands that matcher to the shared `StatusCodeBasedLoggingFilter` base. That base runs `ctx.next(...)`, reads `response.statusCode()`, and prints only if `matcher.matches(statusCode)`. The validation-side shortcut is implemented differently. `then().log().ifError()` is an `if (response.statusCode() >= 400)` branch that prints the response when it is taken. The `expect().log().ifError()` form on the response specification takes the filter route but with `greaterThanOrEqualTo(400)` and no upper bound. Either way, the ceiling that `ErrorLoggingFilter` has is simply absent. ## Which codes actually differ The gap is narrow and exactly where production failures live: - **400 to 499** -- both print. Client errors are covered identically. - **500 exactly** -- both print. The bound is `lessThanOrEqualTo`, not `lessThan`, so 500 is inside the window. - **501 through 599** -- only `ifError()` prints. Not Implemented, Bad Gateway, Service Unavailable and Gateway Timeout all fall outside `ErrorLoggingFilter`. - **1xx, 2xx, 3xx** -- neither prints, which is the intended behaviour for both. On a museum ticketing API, that gap is not academic. `POST /v1/tickets` returns 503 when the payment gateway is unreachable, and `GET /v1/exhibitions/{exhibitionId}/slots` returns 504 when the inventory service times out. Those are exactly the failures a build engineer needs the body for, and exactly the ones `ErrorLoggingFilter` stays quiet about. ## What each one prints, not just when The *when* is only half the question; interviewers often follow up with the *what*. 1. `ErrorLoggingFilter` prints the **response only**. It never prints the request. Its base class is constructed with `LogDetail.ALL` for the response, meaning status line, headers and body -- but the request that produced them is not in the output. 2. The base class builds its printer with an empty blacklisted-header set, so response headers are printed as they arrived. 3. `then().log().ifError()` likewise prints the response, at full detail, and likewise not the request. So neither of them, on its own, tells you what you sent. That is a separate decision about request logging, and worth saying out loud rather than assuming the error filter covers both directions. ## Getting the coverage you actually meant If what you want is *"print the response whenever the call failed, whatever the code"*, do not reach for `ErrorLoggingFilter`. `ResponseLoggingFilter` accepts any `Matcher<Integer>`, so the correct filter-shaped answer is one line: ```java given() .filter(new ResponseLoggingFilter(greaterThanOrEqualTo(400))) .get("/v1/exhibitions/{exhibitionId}/slots", "pompeii") .then() .statusCode(200); ``` Other shapes of the same idea, all real: - `ResponseLoggingFilter.logResponseIfStatusCodeMatches(anyOf(is(409), greaterThanOrEqualTo(500)))` -- narrow it to the codes a given suite cares about. - `ResponseLoggingFilter.logResponseIfStatusCodeIs(409)` -- exactly one code, for a suite that only cares about the double-check-in conflict on `POST /v1/visits/{ticketCode}/check-in`. - `new ResponseLoggingFilter(LogDetail.BODY, System.err, greaterThanOrEqualTo(400))` -- body only, on the error stream. - `ResponseLoggingFilter.logResponseTo(System.err)` -- every response, unfiltered, when you are debugging one test rather than running a suite. - `ErrorLoggingFilter.logErrorsTo(System.err)` -- the original window, redirected off standard output, for a suite that genuinely only cares about client errors. Any of those can go on a shared `RequestSpecBuilder` so the whole suite agrees on one predicate, which is usually better than each test choosing its own. ## Why the mismatch exists at all `ErrorLoggingFilter` is old. Its 400-to-500 window predates the habit of treating every 5xx as a first-class test outcome, and it has never been widened, because widening it would silently change what existing suites print. That is a reasonable library decision and a trap for anyone who reads the class name and assumes the semantics. The lesson generalises past this one class: on a DSL where the request side, the response side and the filter package all offer similar-sounding names, **read the predicate, do not infer it from the noun**. The one-sentence version to carry into an interview: `ErrorLoggingFilter` is a closed interval of 400 to 500, `log().ifError()` is an open-ended 400 and up, and if you care about 503s you want `ResponseLoggingFilter` with your own matcher.
- Does ErrorLoggingFilter print the request that caused the failure?No. It prints the response only, at full detail -- status line, headers and body -- and the request that produced it is not in the output. Request logging is a separate decision: register a `RequestLoggingFilter` as well, or reach for the request-side log shortcut. Assuming the error filter covers both directions is a common misreading of the class name.
- Where does ErrorLoggingFilter write its output, and can you redirect it?The no-argument constructor writes to `System.out`. Both `new ErrorLoggingFilter(printStream)` and the static `ErrorLoggingFilter.logErrorsTo(printStream)` take any `PrintStream`, so sending failures to `System.err` or to a per-test buffer is a constructor argument, not a configuration setting.
saying these in an interview costs you the question
- Saying ErrorLoggingFilter covers every 4xx and 5xx status
- Treating log().ifError() and ErrorLoggingFilter as interchangeable
- Thinking the upper bound excludes 500 rather than including it
- Claiming ErrorLoggingFilter prints the failing request too
- Expecting a configuration setting to widen the filter's status range