skip to content

In REST Assured, which status codes trigger ErrorLoggingFilter, and how does that differ from then().log().ifError()?

level: middleimportance: should knowfreq 42%

answer

  1. two predicates, one upper bound
  2. allOf on the filter, plain compare on the DSL
  3. 500 inclusive, 503 outside
  4. ResponseLoggingFilter takes any Matcher<Integer>

basics

~20 s

REST 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 s

They 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 lines
java
import 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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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