Request Headers and Content Types

8 min read

Headers are how a client and server communicate everything except the payload. Auth credentials, content negotiation, request tracing, caching — all live in headers. The API Testing Masterclass lesson on HTTP methods, status codes, and headers covered the conceptual side; this lesson is the Rest Assured DSL for setting them, plus the half-dozen headers every API tester ends up reaching for. Get this right and your auth, content negotiation, and tracing all become one-liners.

Setting one header at a time

The simplest form — header(name, value):

given()
    .header("Accept", "application/json")
    .header("Authorization", "Bearer " + token)
    .header("X-Request-Id", UUID.randomUUID().toString())
.when()
    .get("/users")
.then()
    .statusCode(200);

Each call adds one header. You can call .header(...) as many times as you want — order doesn't matter for HTTP, and Rest Assured preserves them all.

Setting many headers at once

When you have a Map or an existing Headers object:

import io.restassured.http.Header;
import io.restassured.http.Headers;
 
Headers headers = new Headers(
    new Header("Accept", "application/json"),
    new Header("Authorization", "Bearer " + token),
    new Header("X-Custom-Header", "custom-value")
);
 
given()
    .headers(headers)
.when()
    .get("/users");

Or with a Map (often the easiest for parameterised tests):

Map<String, String> headers = Map.of(
    "Accept", "application/json",
    "Authorization", "Bearer " + token,
    "X-Request-Id", UUID.randomUUID().toString()
);
 
given()
    .headers(headers)
.when()
    .get("/users");

Both forms compose with per-test header(...) calls — the Map sets the defaults, individual header() calls override or add.

Content-Type — what your body is

Content-Type tells the server how to parse the body of the request you're sending. Rest Assured has a dedicated contentType() method that takes a ContentType enum value or a raw string:

// JSON — by far the most common
given()
    .contentType(ContentType.JSON)
    .body("{\"name\":\"Alice\"}")
    .post("/users");
 
// URL-encoded form (login forms, OAuth token endpoints)
given()
    .contentType(ContentType.URLENC)
    .formParam("username", "alice")
    .formParam("password", "pass123")
    .post("/login");
 
// XML (legacy or SOAP-adjacent endpoints)
given()
    .contentType(ContentType.XML)
    .body("<user><name>Alice</name></user>")
    .post("/users");
 
// Multipart for file uploads (covered in Lesson 4)
given()
    .contentType("multipart/form-data")
    .multiPart("file", new File("photo.jpg"));

The ContentType enum covers the common cases. For exotic types (application/vnd.company.v3+json, anyone?) pass a raw string — .contentType("application/vnd.company.v3+json") works identically.

Accept — what you want back

Accept is the mirror image of Content-Type — it tells the server what format you'd like the response in. Many APIs honour it for content negotiation (Accept: application/xml returns XML even if the default is JSON):

given()
    .accept(ContentType.JSON)        // "I want JSON back, please"
    .contentType(ContentType.JSON)   // "And here's JSON in my body"
.when()
    .post("/users");

The two methods are independent — you can accept one format and contentType another. A common pattern: send application/json and accept application/vnd.api+json for JSON:API-style responses.

The headers every API tester reaches for

Authorization is what you'll set most often. X-Request-Id is what teams forget to send and then can't trace failed requests through their logs. Cache-Control is what makes load tests fail mysteriously when a CDN is silently caching responses.

Asserting on response headers

Setting headers is half the job; reading them is the other half. The Response object exposes them, and then() lets you assert directly:

given()
.when()
    .get("/users/1")
.then()
    .statusCode(200)
    .header("Content-Type", containsString("application/json"))
    .header("X-Request-Id", notNullValue())
    .header("Cache-Control", equalTo("no-cache"));

To capture a header value for use elsewhere:

String requestId = given()
.when()
    .get("/users/1")
.then()
    .extract()
    .header("X-Request-Id");
 
System.out.println("Request ID: " + requestId);

Useful for tests that follow a request through logs, or that need a Location header from a 201 response to fetch the newly-created resource:

String location = given()
    .contentType(ContentType.JSON)
    .body(newUser)
.when()
    .post("/users")
.then()
    .statusCode(201)
    .extract()
    .header("Location");
 
// Follow the Location header
given().when().get(location).then().statusCode(200);

A custom request header for tracing

Production-grade test suites set a unique X-Request-Id on every request so failed runs can be traced through logs. The pattern:

import java.util.UUID;
 
@BeforeMethod
public void attachRequestId() {
    String requestId = "test-" + UUID.randomUUID();
    RestAssured.requestSpecification = new RequestSpecBuilder()
        .addHeader("X-Request-Id", requestId)
        .build();
    System.out.println("Test " + this.getClass().getSimpleName() + " using request ID " + requestId);
}

When a CI run fails three months later, copying the request ID from the test output and grepping the production log files leads straight to the corresponding server-side trace. We'll formalise this in Chapter 6 with a proper logging filter.

Cookies — a special header

Cookies travel as the Cookie request header and Set-Cookie response header, but Rest Assured has dedicated methods that are nicer to use:

given()
    .cookie("session", "abc123")
    .cookie("locale", "en_GB")
.when()
    .get("/profile")
.then()
    .cookie("session", notNullValue());

Most modern APIs use bearer tokens rather than cookies, but cookie-based session auth is still common in monoliths and admin panels.

When the API requires a custom Content-Type

Some APIs use vendor-specific media types like application/vnd.api+json (JSON:API), application/vnd.company.v2+json (versioning), or application/problem+json (RFC 7807 errors). Pass them as raw strings:

given()
    .contentType("application/vnd.company.v2+json")
    .accept("application/vnd.company.v2+json")
    .body(payload)
.when()
    .post("/users");

Rest Assured doesn't constrain the value — whatever string you pass goes on the wire as-is.

⚠️ Common mistakes

  • Forgetting Content-Type on POST/PUT/PATCH. Without it, many servers default to application/x-www-form-urlencoded and silently fail to parse the JSON body. The test sees a 400 and you wonder what's wrong with your JSON. Always set contentType(ContentType.JSON) when you have a JSON body.
  • Confusing Content-Type and Accept. They look similar but say different things: Content-Type describes what you're sending; Accept describes what you want back. A request with no body should have no Content-Type; a request expecting JSON should always have Accept: application/json.
  • Hardcoding a bearer token. Tests that hardcode Authorization: Bearer eyJhbGciOi... break the moment that token expires. Build a TokenProvider (Chapter 4 covers this in depth) that fetches and caches tokens, and inject the token via a shared RequestSpecification.

🎯 Practice task

Wire headers into the test class you've been growing. 25–30 minutes.

  1. In UserApiTest (against JSONPlaceholder), add an Accept: application/json and User-Agent: rest-assured-course/1.0 header to one of your tests. Run it green.
  2. Add .header("X-Request-Id", UUID.randomUUID().toString()) to every test in the class. (For now, copy-paste; we'll factor it out in Chapter 6.) Confirm the suite is still green.
  3. Read a header from the response. GET /users/1 and extract().header("Content-Type"). Print it. Note the format JSONPlaceholder returns (application/json; charset=utf-8 — note the charset).
  4. Write assertResponseContentTypeIsJson(). Use both .contentType(ContentType.JSON) and .header("Content-Type", containsString("application/json")). Note that the first is stricter (it normalises) and the second is more permissive — both work; pick by your team's style.
  5. Authorisation rehearsal. Write a test that adds Authorization: Bearer fake-token-for-testing. JSONPlaceholder doesn't enforce auth — but on a real API, this is exactly what you'd do. Run it; confirm it doesn't break (the header is sent but ignored).
  6. Negative content-type test. POST a JSON body but set contentType(ContentType.XML). Run against a real API (REQRES, for example). Note the response — most will return 415 Unsupported Media Type or 400 Bad Request. This is a real test class that catches API regressions where content-type validation is removed.
  7. Stretch: factor your default headers into a RequestSpecBuilder and call given().spec(commonSpec) in every test. The class should shrink visibly. Save the change — we'll build on it in Chapter 6.

Next lesson: JSON request bodies — strings, Maps, POJOs, and which to use when.

// tip to track lessons you complete and pick up where you left off across devices.