skip to content

Your REST Assured booking test sets baseUri and auth, then calls spec(trekSpec), and both are ignored — why?

level: seniorimportance: must knowfreq 58%

answer

  1. not a fallback layer
  2. merges where you call it
  3. scalars are plain assignment
  4. an unset builder field is not empty
  5. attach first, override afterwards

basics

~20 s

REST Assured's spec(...) merges at the point of the call, and the incoming specification wins on scalars: SpecificationMerger overwrites baseUri, basePath, port and the authentication scheme. Move spec(trekSpec) to the front of the chain. Precedence is call order, not request-beats-spec.

solid answer

~50 s

`spec(...)` is not a fallback layer — it is a merge that runs the moment you call it, and `SpecificationMerger.merge` copies the incoming specification's scalar fields straight over yours. On the request side the overwritten fields are `port`, `baseUri`, `basePath`, `authenticationScheme`, `requestBody`, `urlEncodingEnabled`, `allowContentType`, `addCsrfFilter`, `proxySpecification`, `method` and `path`. None of those assignments is conditional on the incoming value being set. Worse, a spec built by `new RequestSpecBuilder()` is never empty: the constructor snapshots the `RestAssured` statics, so an untouched base URI still carries `http://localhost` and an untouched scheme still carries a `NoAuthScheme`. Those defaults are what overwrite your real values. The fix is ordering. Put the shared spec first — `given().spec(trekSpec).baseUri(...)` — or use `RestAssured.given(trekSpec)`, which is defined as `given().spec(trekSpec)` and therefore leaves every later call in the winning position. Headers and parameters survive either way, which is what makes the failure look so puzzling.

code

java · 16 lines
java
import static io.restassured.RestAssured.given;

// trekSpec came from new RequestSpecBuilder(), so it already carries
// baseUri "http://localhost" and a NoAuthScheme it was never given

// WRONG - the merge happens here and overwrites both scalars
given().baseUri("https://api.llamatrek.example")
    .auth().preemptive().basic("guide", "s3cret")
    .spec(trekSpec)
.when().get("/v2/treks");   // lands on localhost, unauthenticated

// RIGHT - attach first, then override
given().spec(trekSpec)
    .baseUri("https://api.llamatrek.example")
    .auth().preemptive().basic("guide", "s3cret")
.when().get("/v2/treks");

go deeper

for a junior

Remember the habit rather than the field list: put spec(...) first in the chain and write your per-test overrides after it, and this failure never happens to you.

for a middle

Explain that SpecificationMerger assigns scalars unconditionally, and that a RequestSpecBuilder snapshots the RestAssured statics in its constructor so an untouched field is still a real value.

for a senior

Demonstrate the diagnosis: log the resolved URI, find the spec() call that is not first, and check when the builder was constructed relative to the statics it snapshots.

for a principal

Decide the suite-wide convention and enforce it. Either every call starts with the shared spec, or specs set every field they own explicitly so a merge is never ambiguous.

## The mental model that causes the bug Almost everyone reads `spec(...)` as a **fallback layer**: the shared specification supplies defaults, and anything you set explicitly on the request beats it. That model is wrong, and it is wrong in the direction that hides the problem — the request still goes out, it just goes somewhere else. The real rule is one sentence: **`spec(...)` merges at the point of the call, and the incoming specification wins on scalars.** Precedence is decided by call order, not by which object is "more specific". ## What the merge actually does on the request side `RequestSpecificationImpl.spec(RequestSpecification)` immediately calls `SpecificationMerger.merge(this, incoming)`. For scalar fields the merger performs a plain assignment from the incoming spec onto yours: - `port`, `baseUri`, `basePath` - `authenticationScheme` - `requestBody` - `urlEncodingEnabled`, `allowContentType`, `addCsrfFilter` - `proxySpecification` - `method` and `path` None of those assignments is conditional. The merger does not check whether the incoming value is null, blank or "unset" — it copies whatever is there. That is the whole bug. ## Why an "empty" spec still overwrites The second half of the trap is that a specification built by `new RequestSpecBuilder()` is never empty. Its constructor snapshots the `RestAssured` statics as they stand at construction time: `baseURI`, `port`, `basePath`, `authentication`, the filter list, `requestSpecification`, `urlEncodingEnabled`, `config` and `proxy`. So a `trekSpec` on which you only ever called `addHeader(...)` still carries: - `baseUri` = `RestAssured.baseURI`, whose default is `"http://localhost"` - `port` = `RestAssured.port`, whose default is `UNDEFINED_PORT` (`-1`) - `basePath` = `RestAssured.basePath`, whose default is the empty string - `authenticationScheme` = `RestAssured.authentication`, whose default is a `NoAuthScheme` Merge that in after your own `baseUri(...)` and `auth().preemptive().basic(...)` and both are gone: the request lands on `http://localhost` with no credentials. The symptom is usually a connection refusal or a `401`, and neither points at the line that caused it. ## The fix is ordering ```java // WRONG - the merge runs here and copies trekSpec's scalars over yours given().baseUri("https://api.llamatrek.example") .auth().preemptive().basic("guide", "s3cret") .spec(trekSpec) .when().get("/v2/treks"); // RIGHT - attach first, then override given().spec(trekSpec) .baseUri("https://api.llamatrek.example") .auth().preemptive().basic("guide", "s3cret") .when().get("/v2/treks"); ``` The static `given(trekSpec)` gives you the correct shape for free, because it is defined as `return given().spec(trekSpec);` — the merge is already behind you before you write anything else. Adopt one of those two forms as a suite-wide convention and the class of bug disappears. ## What the merger does *not* overwrite The same call is additive for collections, which is why the failure is confusing: your headers, cookies, query parameters, form parameters, named path parameters, multiparts and filters all survive the merge and show up in the request log. Only the scalars vanish, so the log looks two-thirds correct. Auth is a special case worth knowing, because it lives on both sides. The scheme is a scalar and is overwritten. Auth *filters* — the ones `auth().form(...)` installs — are handled in `mergeFilters`: if both specifications carry an auth filter the existing ones are dropped, and if the incoming spec carries an `ExplicitNoAuthScheme` (what `auth().none()` sets) every auth filter is removed outright. Session identifiers get their own reconciliation too: if the incoming specification carries a cookie under the session id name, the merger strips the existing session cookie first so you end up with one session rather than two. ## The same trap on the validation side Attaching a response specification with `expect().spec(bookingCreated)` runs the second overload of the same merger, and it behaves identically: `expectedStatusCode`, `expectedStatusLine`, `contentType`, `bodyRootPath`, `expectedResponseTime` and the response log detail are assigned from the incoming specification, while body matchers, cookie assertions and header assertions accumulate. So `expect().statusCode(202).spec(bookingCreated)` quietly stops expecting `202`. The same remedy applies: attach first, restate afterwards. `then().spec(bookingCreated)` is the exception that proves the rule — it does not merge at all, it validates the response you already hold, so nothing there can be overwritten. ## How to diagnose it in a running suite 1. Print what the request really became. `given().log().uri()` before the verb, or `SpecificationQuerier.query(spec)` on the built spec, shows the base URI and headers that survived. 2. Look for a `spec(...)` call that is not the first thing in the chain — that is the single most common cause. 3. Check when the specification was constructed. A `RequestSpecBuilder` created in a static initialiser snapshots statics that a `@BeforeAll` sets later, so it carries stale defaults. 4. Ask whether the field that disappeared is on the overwrite list above. If it is a scalar, the merge explains it; if it is a header or a parameter, look elsewhere. ## The sentence to remember Attaching a specification is not layering, it is assignment. Put the shared specification first and treat everything after it as the deliberate exception.

  • The query parameter I set before spec(trekSpec) did survive. Why did that one not get overwritten?
    Because parameters are not scalars. `SpecificationMerger.merge` calls `putAll` for request, query, form and named path parameters and `addAll` for multiparts, cookies, headers and filters, so both sides' entries end up on the request. Only the assignment-style fields — base URI, base path, port, scheme, body, method, path — are replaced.
  • A spec built in a static initialiser behaves differently from one built in @BeforeAll. What changes?
    `new RequestSpecBuilder()` snapshots the `RestAssured` statics in its constructor. A builder created in a static initialiser runs before `@BeforeAll` sets `RestAssured.baseURI`, so it captures the defaults and later overwrites the correct values on every merge. Build specifications after the statics they depend on are set, or set every field on the builder explicitly.
  • Does given().spec(trekSpec) mutate trekSpec, so a later test sees the change?
    No. `SpecificationMerger.merge(this, incoming)` writes into the receiver — the fresh specification `given()` just created — and only reads from the incoming one. The shared spec is left untouched and can be reused. `RequestSpecBuilder.addRequestSpecification(...)` is the opposite: it merges into the builder's own spec and does mutate it.

Attaching a specification is a paste over the cursor, not a stylesheet cascade: whatever the pasted block covers is simply gone, no matter how deliberately you typed it a moment earlier.

saying these in an interview costs you the question

  • Saying an explicit per-request call always beats an attached spec
  • Treating spec() as a defaults layer that only fills gaps
  • Assuming a builder with nothing set contributes nothing
  • Blaming a stale RestAssured static instead of the merge order
  • Expecting the spec to be applied only when the request is sent