skip to content

In REST Assured, how can RequestSpecBuilder.setConfig(...) wipe a setting you made in RestAssured.config?

level: seniorimportance: should knowfreq 34%

answer

  1. a flag per config object
  2. object granularity, not field
  3. isUserConfigured decides the winner
  4. chain from the object already in place

basics

~20 s

When a specification is merged into a request, REST Assured chooses between the two configs one config object at a time, using each object's isUserConfigured flag. The winner replaces the whole object, so untouched fields inside it revert to defaults.

solid answer

~40 s

REST Assured merges configuration **per config object, not per field**. Each of the eighteen carries an `isUserConfigured` flag that flips to `true` the moment any of its setters is called. At `given().spec(s)` the merger walks the eighteen types: where the specification's object is user-configured it replaces the request's outright, otherwise the request keeps its own. So if `RestAssured.config` carried `httpClientConfig().setParam("http.connection.timeout", 3000)` and the specification sets `httpClientConfig().reuseHttpClientInstance()`, the specification's `HttpClientConfig` wins whole and the timeout parameter is gone — both settings live in the same object and nothing merges them. Whether an object counts as touched is that `isUserConfigured` flag, and any setter sets it — changing a value is not required. `DecoderConfig` and the other sixteen, untouched by the specification, survive. The fix is to chain from the object already in place: `RestAssured.config().getHttpClientConfig().reuseHttpClientInstance()`.

code

java · 23 lines
java
import static io.restassured.RestAssured.given;
import static io.restassured.config.HttpClientConfig.httpClientConfig;

// global: one connection timeout for the whole marina suite
RestAssured.config = RestAssured.config()
        .httpClient(httpClientConfig().setParam("http.connection.timeout", 3000));

// BROKEN: a fresh HttpClientConfig is user-configured, so at given().spec(...)
// it replaces the global one whole - and the timeout parameter goes with it
RequestSpecification bookings = new RequestSpecBuilder()
        .setBaseUri("https://api.harbourside-marina.test")
        .setConfig(RestAssured.config()
                .httpClient(httpClientConfig().reuseHttpClientInstance()))
        .build();

// FIXED: chain from the HttpClientConfig that is already in place
RequestSpecification fixed = new RequestSpecBuilder()
        .setBaseUri("https://api.harbourside-marina.test")
        .setConfig(RestAssured.config().httpClient(
                RestAssured.config().getHttpClientConfig().reuseHttpClientInstance()))
        .build();

given().spec(fixed).when().post("/bookings").then().statusCode(201);

go deeper

for a junior

Know that attaching a specification can change configuration, and that a config object is replaced as a unit. When a setting seems to vanish, look at what the specification set before blaming the request.

for a middle

Explain the isUserConfigured flag: set by any setter, read by the merger, and evaluated per config object rather than per field.

for a senior

Diagnose it from the symptom. Read the merged config back off the built specification, identify which object won, and fix it by chaining from the existing instance rather than a fresh factory call.

for a principal

Set the convention that decides this before it bites: one owner per config object across the suite, so a specification never has to replace an object somebody else populated.

## Every config object carries a flag All eighteen config classes implement `io.restassured.config.Config`, an interface with one method: `boolean isUserConfigured()`. The value is a final field decided at construction. - The no-arg constructor — which is what the static factories `redirectConfig()`, `httpClientConfig()`, `decoderConfig()` and friends call — sets it to `false`. - Every chained setter returns a **new** instance with the flag set to `true`, whether or not the value you passed differs from the default. - `RestAssuredConfig.isUserConfigured()` aggregates: it returns `true` if any one of the eighteen it holds reports `true`. So the flag records *that you touched the object*, not *that the value changed*. `httpClientConfig().dontReuseHttpClientInstance()` sets the flag even though `false` was already the default. ## What the merger does `given().spec(spec)` runs `SpecificationMerger`, and its configuration step is short: 1. If the specification's whole config reports `isUserConfigured() == false`, nothing happens and the request keeps the config it was seeded with. 2. If the request's config is untouched and the specification's is not, the specification's config replaces it entirely. 3. If both are user-configured, the merger walks the eighteen config types one at a time. For each type it takes the specification's object when that object is user-configured, and the request's object otherwise, and assembles the winners into a new `RestAssuredConfig`. That third branch is the interesting one, and the word doing the damage is **object**. ## Object granularity, not field granularity | What you set globally | What the spec sets | What the request ends up with | |---|---|---| | `DecoderConfig` decoders | nothing on `DecoderConfig` | your global decoders | | `HttpClientConfig` params | nothing on `HttpClientConfig` | your global params | | `HttpClientConfig` params | `reuseHttpClientInstance()` | reuse only — the params are gone | | nothing | `RedirectConfig.maxRedirects(2)` | the spec's redirect config | Row three is the whole question. The two settings are fields of the same class, and the merger never looks inside a config object. The specification's `HttpClientConfig` was built by `httpClientConfig()`, a fresh instance whose parameter map holds only the library's own defaults, and `reuseHttpClientInstance()` copied that map forward. Nothing in the pipeline knows the global object had a timeout in it. The same trap exists one level up. `RestAssured.config().httpClient(httpClientConfig().reuseHttpClientInstance())` looks careful — it starts from the assigned global config — but the argument is still a brand-new `HttpClientConfig`, so the container-level setter replaces the populated object with a bare one. Starting from `RestAssured.config()` protects the other seventeen objects; it does not protect the one you are replacing. ## The marina symptom On a berth-booking suite the report reads like a flake. `POST /bookings` and `GET /bookings/{bookingId}` hang for the platform default instead of failing at three seconds, but only in the tests that attach the bookings specification; `GET /berths`, which uses the plain global config, times out correctly. Nothing in the failure names configuration, and the specification's own source shows a single, innocent-looking line. The reliable way to confirm it is to ask what the built request actually carries rather than reading the setup code: - `SpecificationQuerier.query(spec).getConfig()` returns the merged `RestAssuredConfig`, so you can read `getHttpClientConfig().params()` back and see the missing key. - Check `isUserConfigured()` on the specific config object in both configs — if both say `true`, the specification's object won. - Remove `setConfig(...)` from the builder and rerun; if the behaviour returns, the merge is the cause. ## Keeping the setting There are two shapes that work, and they differ in who owns the object: 1. **Chain from the object already in place.** `RestAssured.config().httpClient(RestAssured.config().getHttpClientConfig().reuseHttpClientInstance())` copies the existing parameter map forward, so both settings end up in the one `HttpClientConfig` that wins the merge. 2. **Put every setting for a given object in one place.** If only the global hook ever touches `HttpClientConfig`, no specification can replace it, and specifications are left owning objects nobody else configures. The second is the better default for a suite several people edit, because it removes the need to remember rule one. Rule one is still worth knowing, because it is the only option once a specification legitimately needs its own value for an object the global hook also populates. ## The other bypass A few settings have a shortcut on the request specification itself, and those do not go through the config objects at all. `given().redirects().max(3)` writes its value straight into the request's HTTP-client parameter map, and `RedirectConfig` is applied afterwards with put-if-absent semantics — it fills in only the keys nobody has set. So the DSL call wins over the config object regardless of scope, which is worth knowing before you spend an afternoon on why a carefully merged `RedirectConfig` appears to be ignored. `HttpClientConfig.params()` is applied the same way, put-if-absent into the same map, so anything already written there by a DSL shortcut also survives it. The practical rule is to pick one surface per setting across the suite — either the config object or the shortcut, never both — so that the question of which one wins never has to be answered from memory.

  • What exactly flips a config object's isUserConfigured flag to true?
    Any of its chained setters, and its public value-taking constructors. `redirectConfig()` and the other no-arg factories produce an object with the flag `false`; `redirectConfig().followRedirects(true)` returns a new object with it `true`, even though `true` was already the default. The flag tracks the fact that you called something, not whether the value moved, which is why a no-op setter can still win a merge.
  • How would you inspect the configuration a built specification will actually contribute?
    `SpecificationQuerier.query(spec)` returns a `QueryableRequestSpecification`, whose `getConfig()` hands back the `RestAssuredConfig` on that specification. From there you can read individual objects — `getHttpClientConfig().params()`, `getRedirectConfig().maxRedirects()` — and call `isUserConfigured()` on each to see which ones will replace the request's at merge time.
  • Does the same object-level merge apply when a specification is attached to another specification?
    Yes. `RequestSpecBuilder.addRequestSpecification(...)` and `given().spec(...)` both run the same merger, so configuration is resolved object by object at every level. Chaining specifications therefore compounds the problem rather than smoothing it: the last user-configured object for a given type wins, and everything else inside that object is discarded with the loser.

saying these in an interview costs you the question

  • Thinks two configs merge field by field
  • Believes the request's own config always beats the specification's
  • Assumes an untouched config object still overwrites the global one
  • Says setConfig only adds to what the request already had
  • Expects a globally set RedirectConfig to override given().redirects().max(...)