skip to content

In REST Assured, why does setting the same custom header twice send two values, and how do you stop it?

level: seniorimportance: must knowfreq 45%

answer

  1. header() appends, it does not assign
  2. two names are special
  3. content-type and accept overwrite
  4. HeaderConfig holds the switch
  5. cookies have no such switch

basics

~10 s

Request headers merge by default, so both values go on the wire; only Content-Type and Accept overwrite. Move the name into the overwrite set with HeaderConfig.overwriteHeadersWithName, or stop setting it twice.

solid answer

~40 s

`header(name, value)` appends rather than assigns. On each call REST Assured asks `HeaderConfig` whether that name overwrites; the shipped answer is yes for exactly two names, `content-type` and `accept`, and no for everything else. So a shared specification that stamps `X-Depot-Id: north-yard` plus a test that sets `X-Depot-Id: south-yard` sends both lines to the dispatch API. Fix it with `config(config().headerConfig(headerConfig().overwriteHeadersWithName("X-Depot-Id")))`, or better, stop setting the header in two places. Two things to know before you reach for the config: `overwriteHeadersWithName(...)` builds a fresh set, so `Content-Type` and `Accept` fall back to merging unless you list them too, and cookies have no equivalent switch — they always accumulate into one `Cookie` header.

code

java · 28 lines
java
import static io.restassured.RestAssured.config;
import static io.restassured.RestAssured.given;
import static io.restassured.config.HeaderConfig.headerConfig;

class DispatchDepotHeaderTest {

    void bothDepotHeadersAreSent() {
        given()
            .header("X-Depot-Id", "north-yard")
            .header("X-Depot-Id", "south-yard")
        .when()
            .get("/v1/routes")            // two X-Depot-Id header lines
        .then()
            .statusCode(200);
    }

    void lastDepotHeaderWins() {
        given()
            .config(config().headerConfig(
                headerConfig().overwriteHeadersWithName("X-Depot-Id", "Content-Type", "Accept")))
            .header("X-Depot-Id", "north-yard")
            .header("X-Depot-Id", "south-yard")
        .when()
            .get("/v1/routes")            // one X-Depot-Id: south-yard
        .then()
            .statusCode(200);
    }
}

go deeper

for a junior

Know that setting a header twice sends it twice, and that Content-Type and Accept behave differently. That is enough to stop you writing a duplicate by accident.

for a middle

Explain the mechanism: header() appends, HeaderConfig decides per name whether earlier entries are dropped, and only content-type and accept are marked for overwrite out of the box.

for a senior

Diagnose the shared-specification version of this and weigh the fixes. Show that you know overwriteHeadersWithName replaces the shipped set, and that removing the duplicate source usually beats reconfiguring the library.

for a principal

Own the convention for who may stamp headers. If one layer owns correlation and depot headers and tests never re-set them, this class of ambiguity disappears without any configuration at all.

## Merge is the default for headers, not overwrite `RequestSpecification.header(name, value)` does not set a slot; it appends an entry to a list of headers. When a second `header("X-Depot-Id", "south-yard")` arrives, REST Assured looks up the name in `HeaderConfig` to decide whether the earlier entry should be discarded. If the configuration says that name overwrites, the earlier entries with that name are dropped; otherwise both survive and the request goes out with two `X-Depot-Id` lines. `HeaderConfig` ships with exactly **two** names marked for overwrite, matched case-insensitively: `content-type` and `accept`. Every other name — including every `X-`-prefixed header a snowplough dispatch API might define — merges. So: - `header("X-Depot-Id", "north-yard").header("X-Depot-Id", "south-yard")` sends both values. - `contentType(ContentType.JSON).contentType(ContentType.XML)` sends only the XML content type. - `accept(ContentType.JSON).accept("text/csv")` sends only `text/csv`. - `header("X-Dispatch-Trace", "a", "b")` — the varargs overload — deliberately records two values under one name in a single call. ## Why this bites in a real suite The two calls are almost never adjacent. The usual shape is a shared `RequestSpecification` that stamps `X-Depot-Id: north-yard` on every request, plus one test that needs the southern depot and sets the header again. Attaching a specification **accumulates** headers rather than replacing them, so the merge default and specification merging point the same way, and the dispatch service sees a header it considers ambiguous. Depending on the service, you get a 400, a silent first-value-wins, or — worst for debugging — a correct-looking response produced from the wrong depot. ## Making a name overwrite ```java given() .config(config().headerConfig(headerConfig().overwriteHeadersWithName("X-Depot-Id"))) .header("X-Depot-Id", "north-yard") .header("X-Depot-Id", "south-yard") .when() .get("/v1/routes"); ``` `overwriteHeadersWithName(String, String...)` takes one name plus any number of extra names and returns a new `HeaderConfig` in which those names replace. Its mirror, `mergeHeadersWithName(String, String...)`, makes the listed names accumulate — which is how you would deliberately send two `Accept` headers if a dispatch endpoint required it. ## The trap inside the switch `HeaderConfig` stores a single map of names to overwrite. `overwriteHeadersWithName(...)` builds a **fresh** map containing only the names you passed — it does not add to the shipped set. So a config built as `headerConfig().overwriteHeadersWithName("X-Depot-Id")` no longer lists `content-type` or `accept`, and those two revert to merging. If you rely on the exception for either, name them explicitly: 1. List every name you want to overwrite in the one call: `overwriteHeadersWithName("X-Depot-Id", "Content-Type", "Accept")`. 2. Or keep the default config and change how the test sets the header instead — for instance by removing the header inside a filter before setting it. 3. Or restructure so the shared specification never sets a header a test needs to override. Option three is usually the healthiest: configuration that silently changes what two other headers do is a poor trade for one overridable name. ## Cookies have no equivalent switch Request cookies always accumulate. `cookie("depot", "north-yard").cookie("depot", "south-yard")` records two cookies, and at send time REST Assured joins every request cookie into a **single** `Cookie` header as `name=value` pairs separated by `"; "`, in insertion order — so the dispatch service receives `Cookie: depot=north-yard; depot=south-yard`. There is no `CookieConfig` update strategy to flip: - `cookie(String, Object, Object...)` adds one entry per value. - `cookies(Map)` and `cookies(Cookies)` append to whatever is already recorded. - The only way to end up with one cookie of a given name is not to record the other one. | what you set twice | what the dispatch API receives | |---|---| | `header("X-Depot-Id", ...)` | two `X-Depot-Id` header lines | | `contentType(...)` | one `Content-Type`, the later value | | `accept(...)` | one `Accept`, the later value | | `cookie("depot", ...)` | one `Cookie` header carrying both pairs | ## Diagnosing it Log the request — the request log prints the headers and cookies actually recorded on the specification, which is where a duplicated name is obvious in one line. If the header is being added by something you do not control, a filter can inspect and remove it before the request is sent. Reaching for the request log first is faster than reasoning about which specification contributed what. ## What to say in an interview Say that headers merge by default, that `Content-Type` and `Accept` are the two exceptions `HeaderConfig` ships, and that `overwriteHeadersWithName(...)` moves a name into the overwrite set. Then add the two details that show you have used it in anger: the switch replaces the default set rather than extending it, and cookies have no such control at all — they always accumulate into one `Cookie` header.

  • Why does the example pass Content-Type and Accept to overwriteHeadersWithName as well?
    Because the method builds a fresh overwrite set from the names you give it rather than adding to the shipped one. Passing only `X-Depot-Id` would leave `content-type` and `accept` out of the set, and calling `contentType(...)` twice would then send two `Content-Type` headers instead of replacing the first.
  • What happens in REST Assured if you set the same request cookie name twice?
    Both are recorded and both are sent. At send time every request cookie is joined into a single `Cookie` header as `name=value` pairs separated by `"; "` in insertion order, so a dispatch call receives `Cookie: depot=north-yard; depot=south-yard`. There is no update-strategy configuration for cookies — the only fix is not to record the second one.

By default a header name works like a guest list: every call adds another line. Content-Type and Accept are name badges instead - writing a second one replaces the first.

saying these in an interview costs you the question

  • Assumes header() assigns a slot rather than appending
  • Thinks every header overwrites by default
  • Cannot name HeaderConfig as the place the behaviour lives
  • Believes overwriteHeadersWithName adds to the shipped default set
  • Expects a cookie update strategy that does not exist
  • Blames the server for rejecting a duplicated request header