In REST Assured, how do you assert on a response header inside the then() block?
answer
- four overloads, one interface
- literal, matcher, mapping function, response-aware
- headers(Map) for several at once
- contentType has its own method
- failure dumps every header
basics
~20 sREST Assured's then() block asserts headers with header(name, expectedValue) for an exact string, header(name, Matcher) for anything looser, and headers(Map) for several at once. Content-Type has its own method, contentType(ContentType.JSON). A failed check throws AssertionError and prints every header received.
solid answer
~50 s`ValidatableResponseOptions`, the interface behind `then()`, declares four header overloads. `header("Rental-Catalog-Rev", "rev-2026-04-11")` compares the value as an exact string; `header("Rental-Catalog-Rev", startsWith("rev-2026"))` takes any Hamcrest matcher; `header("RateLimit-Remaining", Integer::parseInt, greaterThan(0))` maps the raw text through a `Function` first, so a numeric header is compared as a number; and `header("Location", response -> endsWith(response.path("rentalId")))` takes a `ResponseAwareMatcher` that can read the response it is validating. `headers(Map)` and the varargs `headers("A", "1", "B", matcher)` register several at once, mixing literals and matchers freely; a plain literal is simply wrapped in `equalTo`. `Content-Type` has a method of its own, `contentType(...)`, because the media type also decides how REST Assured parses the body. The name lookup is case-insensitive and the matcher always receives the raw `String` value, parameters included. A failure throws `AssertionError` naming the header, the expectation, the value seen and every header the response carried.
code
java · 16 linesimport static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
import io.restassured.http.ContentType;
given()
.baseUri("https://api.paddleport.example/v1")
.queryParam("launchSite", "harbour-quay")
.when()
.get("/kayaks")
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.header("Rental-Catalog-Rev", startsWith("rev-2026"))
.header("RateLimit-Remaining", Integer::parseInt, greaterThan(0))
.headers("RateLimit-Limit", "120",
"Cache-Control", containsString("no-store"));go deeper
Be ready to write a header expectation from memory: name plus literal, or name plus Hamcrest matcher, chained after statusCode in the then() block.
Explain what the matcher actually receives - the raw String value, looked up case-insensitively - and when the mapping-function overload is the right one.
Show judgment about which response metadata belongs in a suite at all, and how to keep header expectations from failing on harmless value drift.
Own the convention: which headers form the contract every service test must assert, and where those expectations live so hundreds of tests do not each invent their own.
## What a header assertion actually reads `then()` hands you a `ValidatableResponse`, and every check you chain onto it is declared on `ValidatableResponseOptions`. A header check registers the header name together with a Hamcrest matcher; when REST Assured validates the response it looks that name up in the parsed `Headers` object and runs your matcher against the value it finds. Two consequences fall straight out of that. The lookup is **case-insensitive**, so `header("content-type", ...)` and `header("Content-Type", ...)` behave identically. And unless you interpose a mapping function, the value handed to your matcher is the raw **`String`** — the field value exactly as it arrived, parameters and all. Nothing here touches the outgoing call. `given().header(...)` writes a header on `RequestSpecification`; `then().header(...)` checks one on `ValidatableResponseOptions`. Same word, opposite sides of the DSL. A catalogue call against the kayak-rental API answers like this: ``` HTTP/1.1 200 OK Content-Type: application/json;charset=utf-8 RateLimit-Remaining: 118 Rental-Catalog-Rev: rev-2026-04-11 ``` ## The four `header(...)` overloads | overload | what you pass | what runs | |---|---|---| | `header(String, String)` | a literal value | wrapped in `equalTo`, exact string comparison | | `header(String, Matcher<?>)` | any Hamcrest matcher | matcher applied to the raw value | | `header(String, Function<String,V>, Matcher<? super V>)` | a converter, then a matcher | value mapped first, then matched | | `header(String, ResponseAwareMatcher<R>)` | a lambda over the response | the matcher is built from the response itself | - `header("Rental-Catalog-Rev", "rev-2026-04-11")` is the exact-string form; it is the one that breaks first, and sometimes for no reason worth failing a build over. - `header("Rental-Catalog-Rev", startsWith("rev-2026"))` accepts any Hamcrest matcher, which is how you assert a shape instead of a value. - `header("RateLimit-Remaining", Integer::parseInt, greaterThan(0))` maps the raw text through a `Function` before matching, so a numeric field is compared as a number rather than as characters. - `header("Location", response -> endsWith("/rentals/" + response.path("rentalId")))` builds the expectation out of the very response under validation, which is how you assert that a header agrees with the body beside it. ## Checking several headers in one call - `headers(Map<String, ?>)` takes a map whose values may be plain strings or matchers, mixed freely in the same map. - `headers("RateLimit-Limit", "120", "Rental-Catalog-Rev", startsWith("rev-"))` is the varargs spelling of the same thing: name, value, name, value. - A plain string value is wrapped in `equalTo`, so the map form is no looser than the single-header form — it is shorthand, not a different comparison. - Each entry becomes its own registered check, so a map of four headers is four assertions and four possible failure messages. ## `Content-Type` gets a method of its own Content type is rarely asserted through `header("Content-Type", ...)`, because REST Assured gives it `contentType(ContentType)`, `contentType(String)` and `contentType(Matcher<? super String>)`. Those three are deliberately not equivalent in strictness: the `ContentType` enum form resolves the media type into a family, the `String` form is a prefix comparison, and the `Matcher` form sees the raw header including any `;charset=utf-8`. Reach for whichever strictness you actually mean. ## Reading the failure When a header check fails, REST Assured throws an `AssertionError` whose message names the header, prints the matcher's description, prints the value it saw, and then dumps every header the response carried, one per line: ``` Expected header "Rental-Catalog-Rev" was not "rev-2026-05", was "rev-2026-04-11". Headers are: Content-Type=application/json;charset=utf-8 RateLimit-Remaining=118 Rental-Catalog-Rev=rev-2026-04-11 ``` - A header that is simply absent is reported with the value `"null"` — there is no separate "header not present" error, so read the dumped list to tell "wrong value" from "never sent". - The dump is what makes a header assertion cheap to debug: you do not need to rerun with logging to find out what the service actually returned. ## What is worth asserting Header checks earn their place on the metadata that carries contract meaning and that a body check can never see — the media type a client will parse with, a rate-limit budget, a `Location` pointing at the resource a `POST` created, a catalogue revision a caching client depends on. They are also where a lot of brittle tests come from, so prefer a matcher over a literal for anything that legitimately moves, use the mapping-function form for anything numeric, and leave headers that no consumer reads out of the suite entirely.
- Does then().header(name, matcher) care about the case of the header name?No. REST Assured resolves the name against the parsed `Headers` object with a case-insensitive comparison, so `header("content-type", ...)` and `header("Content-Type", ...)` are the same assertion. Only the name is case-insensitive; the value is handed to your matcher exactly as it arrived, so a matcher such as `equalTo("NO-STORE")` will still fail against `no-store`.
- What does then().header(name, expectedValue) report when the response never sent that header?It fails with the value rendered as `"null"` — `Expected header "Rental-Catalog-Rev" was not "rev-2026-04-11", was "null"` — followed by a dump of every header the response did carry. There is no distinct missing-header error, so the dumped list is how you tell an absent header from a wrong one.
- Why prefer header(name, Integer::parseInt, greaterThan(0)) over header(name, equalTo("118"))?The mapping-function overload converts the raw value before matching, so you assert the property you actually care about — a remaining budget above zero — instead of pinning one exact count that changes on every run. The literal form is a string comparison and turns a healthy value into a failure the moment the number moves.
saying these in an interview costs you the question
- Thinking given().header() and then().header() are the same method
- Assuming a header matcher receives a parsed number rather than a String
- Believing header names must match the response's capitalisation exactly
- Using an exact literal for values that legitimately change every run
- Expecting a missing header to produce its own distinct error type