Skip to content

REST Assured 7.0.0

REST Assured 7.0.0 no longer includes a path engine or Groovy, and sends the requests with the JDK’s own HTTP client instead of Apache HttpClient 4. GPath, the path syntax REST Assured has always used, is a dependency that you add yourself, and so is Apache HttpClient 5 if you want it instead of the JDK client. 7.0.0 also adds a second path engine, JSONPath with Jayway, and clients, so that tests that run in parallel can each have their own settings.

7.0.0-RC1 is the release candidate for 7.0.0. Please try it, and report what doesn’t work in the issue tracker.

  • Path engines. rest-assured no longer includes a path engine, and so no longer brings Groovy. Add json-path-gpath, and xml-path-gpath for XML and HTML, and your paths keep working. They are the new names of json-path and xml-path. See Upgrading to 7.0 and Path engines.
  • JSONPath. A second JSON path engine, json-path-jayway, evaluates JSONPath with Jayway JsonPath, such as $.store.book[?(@.price < 10)].title. See JSONPath with Jayway.
  • The HTTP client. The JDK’s java.net.http.HttpClient sends the requests, from the new http-client-jdk module, which rest-assured depends on. Apache HttpClient 4, and with it httpmime, httpcore, commons-codec and commons-logging, is no longer a dependency of any REST Assured artifact (#1862, #1696). Add http-client-apache5 to send the requests with Apache HttpClient 5 instead, for example for NTLM or the Host header, which the JDK client can’t send. httpClientConfig().client(..) picks the client and customizeClient(..) customizes it. See The HTTP client.
  • Clients for parallel tests. RestAssured.useClient(..) binds a RestAssuredClient to the thread of a test, so tests that run in parallel can each have their own settings and keep using plain given() (#534). given(client) names a client for one request. RestAssuredMockMvcClient and RestAssuredWebTestClientClient do the same for Spring, and Kotlin gets Given(client) { } and Scala Given(client)(..). See Clients for parallel tests.
  • Specifications. Sending a request, or validating a response, no longer changes the specification, so one specification can be used from many threads (#945). Specifications get the static settings, or those of the client, when they’re used, instead of copying the static settings when they’re built (#1977). See Specifications no longer change when sent or validated and Where requests get their settings.
  • Jackson 3. json-schema-validator validates with the networknt json-schema-validator 3.0.9, which brings Jackson 3, and the java-json-tools validator is optional. JsonPath now prefers Jackson 3, like the object mapping, so response.jsonPath().getObject(..) and response.as(..) use the same Jackson. See JSON Schema validation and Jackson 3.
  • OAuth 1 no longer needs scribejava: REST Assured signs the requests itself. See OAuth 1 without scribejava.
  • Removed APIs. The APIs that take or return HttpClient 4 types, deprecated in 6.3.0, HTTPBuilder, the root path methods deprecated since 4.0.0, such as root(..), and Jackson 1 support are removed. The OpenRewrite recipe io.restassured.UpgradeToRestAssured7 changes most of this in your Java code. See Removed APIs and Let OpenRewrite do it.
  • Logging. The logging filters take their settings from the config of the request when they log, and every response log now hides the blacklisted headers, not only then().log(). See Logging.
  • Dependencies. Hamcrest 3.0, Groovy 4.0 to 6.x for the GPath modules, Jayway JsonPath 3.0.0 for json-path-jayway, and a Java 17 execution environment in OSGi. See Other changes.

Start from REST Assured 6.3.0 without deprecation warnings, as Preparing for REST Assured 7.0 describes. Then change the version to 7.0.0-RC1 and, if your tests use paths, add a path engine. Or let OpenRewrite do it. Which module do I need lists every module, what it’s for and how to keep their versions in line.

REST Assured 7.0.0 no longer brings json-path and xml-path, and so no longer brings Groovy either. If your tests use paths, such as body("lotto.lottoId", equalTo(5)), path(..), extract().path(..) or jsonPath(), add GPath to your test dependencies. Your paths keep working unchanged:

<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>json-path-gpath</artifactId>
<version>7.0.0-RC1</version>
<scope>test</scope>
</dependency>

Gradle:

testImplementation("io.rest-assured:json-path-gpath:7.0.0-RC1")

Tests that use paths on XML or HTML, or call xmlPath() or htmlPath(), also need xml-path-gpath:

<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>xml-path-gpath</artifactId>
<version>7.0.0-RC1</version>
<scope>test</scope>
</dependency>

Gradle:

testImplementation("io.rest-assured:xml-path-gpath:7.0.0-RC1")

Without a path engine, the first path fails with a message that names the dependency to add:

java.lang.IllegalStateException: REST Assured can't evaluate the path "lotto.lottoId" because no JSON path engine is on the classpath.

jsonPath(), xmlPath() and htmlPath() fail the same way: “REST Assured can’t run jsonPath() because no JSON path engine is on the classpath.” See Path engines for what needs which engine. For JSON, the message also offers JSONPath with Jayway, which suits new tests written in JSONPath rather than the paths you have.

If you use rest-assured-all, you don’t need to do anything: it still contains json-path-gpath and xml-path-gpath and depends on Groovy.

json-path and xml-path are renamed json-path-gpath and xml-path-gpath, with the same packages and classes. If you depend on json-path or xml-path directly, switch to the new names. In 7.x the old names are relocation poms that resolve to the new ones at the same version, so a build that still uses them keeps working: Maven prints a warning, and Gradle resolves them silently. The old names are removed in REST Assured 8.0.0.

Spring Boot 3’s dependency management pins rest-assured, rest-assured-common, json-path and xml-path to REST Assured 5.5.x. Set the rest-assured.version property, so that all of them change together. Spring Boot 3.5 imports rest-assured-bom at that version, so the new modules, such as json-path-gpath and http-client-apache5, get it too:

<properties>
<rest-assured.version>7.0.0-RC1</rest-assured.version>
</properties>

With Gradle and the Spring dependency management plugin: ext["rest-assured.version"] = "7.0.0-RC1" in build.gradle.kts. Without Spring Boot, import io.rest-assured:rest-assured-bom instead to keep the REST Assured artifacts on the same version, see Keep the versions in line.

If you only change the version of rest-assured, Spring Boot 3 keeps json-path 5.5.x on the classpath. Its classes are in the same packages as those of json-path-gpath, so REST Assured stops with a message that tells you to replace it with json-path-gpath or to set rest-assured.version, instead of failing later with a NoSuchMethodError. Likewise, REST Assured now checks that rest-assured-common, json-path-gpath and xml-path-gpath have the same major and minor version as rest-assured, and fails with a message that names both versions. A different patch version only logs a warning.

Spring Boot 4 doesn’t manage REST Assured, so it neither pins its version nor reads a rest-assured.version property. Set the version on each REST Assured dependency, or import rest-assured-bom and leave the versions out:

<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured-bom</artifactId>
<version>7.0.0-RC1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

Gradle:

testImplementation(platform("io.rest-assured:rest-assured-bom:7.0.0-RC1"))

Spring Boot 4 manages some libraries that REST Assured’s modules use, and the versions it picks work: Groovy 5.0.x for json-path-gpath and xml-path-gpath, Jayway JsonPath 2.10.0 for json-path-jayway, Apache HttpClient 5.5.x (Spring Boot 4.0) or 5.6.x (Spring Boot 4.1) for http-client-apache5, and Kotlin 2.2 or later for the Kotlin extensions. It manages both Jackson 2 and Jackson 3. With both on the test classpath, REST Assured uses Jackson 3, see JSON Schema validation and Jackson 3.

The rest-assured-openrewrite module contains the OpenRewrite recipe io.restassured.UpgradeToRestAssured7. It:

  • runs io.restassured.MigrateRestAssured6Deprecations, which moves your Java code from the APIs deprecated in 6.3.0 (see Preparing for REST Assured 7.0),
  • removes the SSL port settings, renames the root path methods deprecated since REST Assured 4.0.0, such as root(..) to rootPath(..), and replaces ContextAuthenticationScheme with AuthenticationScheme (see Removed APIs),
  • changes json-path and xml-path to json-path-gpath and xml-path-gpath in Maven and Gradle builds,
  • upgrades the io.rest-assured dependencies to 7.0.0-RC1, including a version property such as rest-assured.version,
  • and adds json-path-gpath where your Java code evaluates paths, such as body("a.b", ..), path(..), rootPath(..) or jsonPath(), and xml-path-gpath where it calls xmlPath() or htmlPath() or uses XmlPath. They get the scope (or Gradle configuration) and version of your REST Assured dependency, often ${rest-assured.version}.

A path on an XML body looks just like a path on a JSON body, so the recipe can’t tell that a test needs xml-path-gpath for it. Add xml-path-gpath yourself if your tests use paths on XML or HTML. The recipe changes Java code only, so change Kotlin, Groovy and Scala code yourself.

With Maven, run this in your project:

Terminal window
mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
-Drewrite.recipeArtifactCoordinates=io.rest-assured:rest-assured-openrewrite:7.0.0-RC1 \
-Drewrite.activeRecipes=io.restassured.UpgradeToRestAssured7

With Gradle, add the OpenRewrite plugin and the recipe to build.gradle.kts:

plugins {
id("org.openrewrite.rewrite") version "latest.release"
}
rewrite {
activeRecipe("io.restassured.UpgradeToRestAssured7")
}
dependencies {
rewrite("io.rest-assured:rest-assured-openrewrite:7.0.0-RC1")
}

Then run gradle rewriteRun. Review the changes with git diff before you commit them.

  • jsonConfig().numberReturnType(JsonPathConfig.NumberReturnType.BIG_DECIMAL). JsonConfig no longer knows json-path’s enum. Use io.restassured.common.config.NumberReturnType instead.
  • Storing the result of an ObjectMapperConfig factory getter, such as jackson2ObjectMapperFactory(), or of JsonConfig.numberReturnType(), in a variable of a json-path or xml-path type. They now return the rest-assured-common types in io.restassured.common.mapper.factory and io.restassured.common.config. The factories you set still work, since the old factory types extend the new ones.
  • Jackson 1. ObjectMapperType.JACKSON_1, JsonParserType.JACKSON_1, Jackson1ObjectMapperFactory, jackson1ObjectMapperFactory(..) and JsonPath.using(Jackson1ObjectMapperFactory) are removed, and Jackson 1 on the classpath is no longer used for object mapping. Use Jackson 2 or 3.
  • Code that calls jsonPath(), xmlPath() or htmlPath(), or uses JsonPath or XmlPath, needs json-path-gpath or xml-path-gpath to compile.
  • Code that uses the APIs that take or return HttpClient 4 types, or HTTPBuilder. See Removed APIs.
  • Code that uses HttpClient 4 classes itself, which REST Assured no longer brings. Add org.apache.httpcomponents:httpclient to your own build if you need them.
  • The root path methods deprecated since REST Assured 4.0.0, such as root(..), appendRoot(..) and JsonPath.setRoot(..). Use rootPath(..), appendRootPath(..) and setRootPath(..), see Removed APIs.

The OpenRewrite recipe changes the first two, the root path methods, and the SSL and multipart methods among the removed APIs.

Response still has jsonPath(), xmlPath() and htmlPath(), which return JsonPath and XmlPath. Code that never calls them runs without a path engine. But reflection over all the methods of Response, which is what Mockito’s mock(Response.class) and Spock’s Mock(Response) do, loads JsonPath and XmlPath. If you mock Response, keep json-path-gpath and xml-path-gpath on the test classpath.

REST Assured 7.0.0 has no path engine by default. There are two:

  • GPath, the path syntax REST Assured has always used, such as lotto.winners.winnerId. It comes in two modules: json-path-gpath for JSON, and xml-path-gpath for XML and HTML. They depend on Groovy, which REST Assured itself no longer needs.
  • JSONPath, such as $.lotto.winners[*].winnerId, with Jayway JsonPath, in json-path-jayway. JSON only, and without Groovy.

Pick GPath if you’re upgrading from 6.x: your paths keep working unchanged.

You use You need
A path on a JSON body: body("a.b", ..), path(..), extract().path(..), rootPath(..) json-path-gpath
A path on an XML or HTML body xml-path-gpath
jsonPath() or JsonPath json-path-gpath
xmlPath(), htmlPath() or XmlPath xml-path-gpath
Anything else, such as statusCode(..), headers, cookies, body(containsString(..)), hasXPath(..), JSON schema validation or as(Greeting.class) Nothing

The same goes for RestAssuredMockMvc, RestAssuredWebTestClient and the Kotlin and Scala extensions.

json-path-gpath, xml-path-gpath and rest-assured-all are built with Groovy 5.1.3 and depend on it. They work with Groovy 4.0, 5.x and 6.x, which CI tests (4.0.33, 5.0.8 and 6.0.0), so the Groovy version that Spring Boot manages works as it is: 4.0.x in Spring Boot 3.5, and 5.0.x in Spring Boot 4. They’re built with Groovy 5 because code compiled by Groovy 6 doesn’t run on Groovy 4 or 5. In OSGi, the bundles import Groovy from 4.0 up to, but not including, 7.0.

With Groovy 6, two things differ:

  • A JSON body that is just a string with an invalid escape, such as "\x", fails to parse, with Groovy’s groovy.json.JsonException as the cause, instead of giving null. The same escape inside an object or an array fails with every Groovy version.
  • An XML body nested more than 1000 elements deep fails, unless the system property jdk.xml.maxElementDepth is set.

json-path-jayway evaluates JSON paths in body(..), path(..), extract().path(..), rootPath(..), equalToPath(..) and ResponseAwareMatchers with Jayway JsonPath:

<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>json-path-jayway</artifactId>
<version>7.0.0-RC1</version>
<scope>test</scope>
</dependency>

Gradle:

testImplementation("io.rest-assured:json-path-jayway:7.0.0-RC1")
get("/lotto").then().body("$.lotto.winners[*].winnerId", hasItems(23, 54));
get("/store").then().body("$.store.book[?(@.price < 10)].title", hasItems("Sayings of the Century", "Moby Dick"));

A path may leave out the leading $., so lotto.lottoId works too. jsonPath() and JsonPath are GPath. With Jayway, use JaywayPath.from(response.asString()), which returns Jayway’s DocumentContext configured like the module:

List<Integer> winnerIds = JaywayPath.from(get("/lotto").asString()).read("$.lotto.winners[*].winnerId");

The module needs Jayway JsonPath 2.8.0 or later, and brings 3.0.0, which needs Java 17. A version that your build manages, such as Spring Boot 4’s 2.10.0, keeps working. If dependency management sets a version older than 2.8.0, the module fails with a message that says so. Jayway logs through SLF4J, so without an SLF4J provider on the classpath you see SLF4J’s three-line “No SLF4J providers were found” warning once. Add a provider such as slf4j-simple or logback-classic in test scope to replace it with real logging.

With only one of json-path-gpath and json-path-jayway on the classpath, REST Assured uses that one. With both, it uses GPath and logs once, at INFO level through the io.restassured System.Logger, that it does. To use Jayway, set JsonConfig.pathEngine(..):

// For all tests
RestAssured.config = RestAssured.config().jsonConfig(jsonConfig().pathEngine(JsonPathEngine.JAYWAY));
// For one request
given().config(config().jsonConfig(jsonConfig().pathEngine(JsonPathEngine.JAYWAY))). ..
// For a request or response specification
RequestSpecification requestSpec = new RequestSpecBuilder().setConfig(config().jsonConfig(jsonConfig().pathEngine(JsonPathEngine.JAYWAY))).build();
ResponseSpecification responseSpec = new ResponseSpecBuilder().setConfig(config().jsonConfig(jsonConfig().pathEngine(JsonPathEngine.JAYWAY)))
.expectBody("$.lotto.winners[*].winnerId", hasItems(23, 54)).build();

A response specification’s config outranks the request’s, which outranks RestAssured.config. The configs are merged per type, so the JsonConfig of a response specification replaces the request’s whole JsonConfig, including a numberReturnType(..) set globally. That lets one test class move to Jayway while the others keep GPath. pathEngine(..) with an engine whose module isn’t on the classpath fails with “JsonConfig.pathEngine is JAYWAY but io.rest-assured:json-path-jayway isn’t on the classpath.”

The same goes for RestAssuredMockMvc and RestAssuredWebTestClient, with their own config.

  • Lists. GPath’s lotto.winners.winnerId collects winnerId from each element of the winners list. In JSONPath that’s lotto.winners[*].winnerId or $..winnerId. With Jayway, lotto.winners.winnerId fails with “JSONPath (Jayway) can’t evaluate lotto.winners.winnerId: lotto.winners is a list, so use lotto.winners[*].winnerId.” When GPath isn’t on the classpath, the message also suggests adding json-path-gpath, in case the path comes from a 6.x test.
  • Missing values. A missing property gives null in both engines, so body("missing", nullValue()) works with both. A filter or wildcard that matches nothing gives an empty list in Jayway.
  • GPath syntax. Closures such as findAll { it.price < 10 }, GPath methods such as unique() or collect { }, and a quoted key after a dot, such as store.'my-key', aren’t JSONPath and fail as an invalid path. Use a filter, such as [?(@.price < 10)], and bracket notation, such as store['my-key']. Jayway has length(), and accepts size(). It applies a function after [*], such as sum(), to each element, so aggregate over a deep scan instead, such as $..price.sum().
  • Keys that start with $. A leading $schema is the key $schema, as in GPath. Use $['$schema'] to make it explicit.
  • Numbers follow JsonConfig.numberReturnType(..) as with GPath: FLOAT_AND_DOUBLE by default, and BIG_DECIMAL keeps the scale, so 12.50 stays 12.50.
  • Invalid JSON. Jayway’s parser is strict. GPath reads some invalid JSON that Jayway fails on: a trailing comma ([1,]), a leading zero ([01]), a raw control character in a string, and anything after the document ({"a":1}garbage).

The REST Assured bundles require a Java 17 execution environment (osgi.ee JavaSE 17), which every Java 17 framework provides.

The rest-assured bundle resolves without the path engine bundles, Groovy and SPI Fly. Install the json-path-gpath and xml-path-gpath bundles (and Groovy 4.0 to 6.x), or the json-path-jayway bundle, to use paths. They’re found even if they’re installed after rest-assured.

json-path-jayway needs the Jayway JsonPath bundle (json-path) and what it requires: json-smart (net.minidev.json-smart), accessors-smart (net.minidev.accessors-smart), ASM (org.objectweb.asm) and slf4j-api (slf4j.api). With Jayway JsonPath 2.9.0 and later, that’s slf4j-api 2, which in turn requires an SLF4J provider bundle, such as slf4j-simple (slf4j.simple), and an OSGi ServiceLoader Mediator, such as Apache Aries SPI Fly (org.apache.aries.spifly.dynamic.bundle). Jayway JsonPath 2.8.0 needs slf4j-api 1.7 and an SLF4J 1.7 binding bundle, such as slf4j-simple 1.7, but no SPI Fly. Jayway JsonPath 3.0.0, which json-path-jayway brings, also requires the Jackson 3 bundles tools.jackson.core.jackson-core and tools.jackson.core.jackson-databind, and com.fasterxml.jackson.core.jackson-annotations 2.x, which jackson-databind 3 imports.

REST Assured sends the requests with an HTTP client module. There are two:

  • http-client-jdk uses the JDK’s java.net.http.HttpClient and needs nothing else. rest-assured depends on it, so you don’t have to add anything.
  • http-client-apache5 uses Apache HttpClient 5. It behaves more like Apache HttpClient 4, which REST Assured 6 used, and does some things that the JDK client can’t. You add it yourself.

To send the requests with Apache HttpClient 5, add http-client-apache5 to your test dependencies:

<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>http-client-apache5</artifactId>
<version>7.0.0-RC1</version>
<scope>test</scope>
</dependency>

Gradle:

testImplementation("io.rest-assured:http-client-apache5:7.0.0-RC1")

REST Assured then uses it instead of the JDK client, unless you pick another client (see Picking the client). rest-assured-bom manages its version. Like the other REST Assured modules, it must have the same major and minor version as rest-assured, or the request fails with a message that names both versions.

Like Apache HttpClient 4 in REST Assured 6, it creates a client, and so a new connection, for each request, gives the response headers in the order and case the server sent them, with the server’s reason phrase, and sends several Cookie headers as they are. It sends the Host, Connection, Upgrade and Expect headers of a request, answers NTLM challenges (auth().ntlm(..)), connects to a proxy with the scheme https over TLS, with the SSL settings of the request, and logs in to a proxy with Basic for an HTTPS request. The JDK client can’t do those. See How Apache HttpClient 5 differs from the JDK client.

REST Assured follows the redirects, answers the Basic and Digest challenges, keeps the cookies and decodes gzip and deflate itself, so HttpClient 5 does none of that, and it retries nothing. Its timeouts are infinite unless httpClientConfig() sets them. Its read timeout covers each read of the body too, and its connect timeout also covers the TLS handshake of a new connection.

The module supports Apache HttpClient 5.2.1 and later, with HttpCore 5.2 and later, and depends on 5.6.5. If dependency management, such as a BOM, sets an older version, or HttpClient 5 is excluded, the request fails with a message that says what’s missing or too old, for example:

java.lang.IllegalStateException: http-client-apache5 needs Apache HttpClient 5.2.1 or later, but 5.1.3 is on the classpath, probably set by a BOM such as Spring Boot's.

REST Assured doesn’t use the JDK client instead, because you added http-client-apache5 on purpose. HttpClient 5 brings httpcore5-h2, which it uses for HTTP/1.1 too, so don’t exclude it. Without it, the request fails with a message that says so.

Apache HttpClient 5 logs through SLF4J, which prints a warning when there’s no SLF4J backend. To get rid of it, add a backend that matches the slf4j-api version on your test classpath, such as slf4j-simple or slf4j-nop, in test scope. HttpClient 5 brings slf4j-api 1.7.36, unless your build manages another version.

If the application under test runs in the same JVM as the tests, for example with @SpringBootTest, adding http-client-apache5 also puts Apache HttpClient 5 on its classpath. Spring’s RestClient and RestTemplate then pick HttpClient 5 too, and HttpClient 5 retries a 503 or 429 response with a Retry-After header once by default. REST Assured’s own requests are never retried, and REST Assured leaves the application’s client alone. To keep the application on the client it had, set its request factory, for example to JdkClientHttpRequestFactory, or set the Spring Boot property that picks it (spring.http.client.factory=jdk in recent versions).

Without a choice, REST Assured uses http-client-apache5 if it’s on the classpath, and otherwise http-client-jdk. In full, REST Assured uses the first of these:

  1. The client that httpClientConfig().client(..) picks, in the config of the request.
  2. The client that the system property restassured.http.client names.
  3. http-client-apache5, if it’s on the classpath.
  4. http-client-jdk.

It logs the client it uses once through the io.restassured System.Logger: at DEBUG level for http-client-jdk when nothing chose it, and otherwise at INFO level, for example “REST Assured uses the HTTP client from io.rest-assured:http-client-apache5 (Apache HttpClient 5.6.5), found on the classpath. To pin a client, use httpClientConfig().client(..) or the system property restassured.http.client.”

Pick a client for all requests, or for one:

RestAssured.config = RestAssured.config().httpClient(httpClientConfig().client(HttpClientType.JDK));
given().config(RestAssured.config().httpClient(httpClientConfig().client(HttpClientType.APACHE5))). ..

Or with a system property, for every request that doesn’t pick one in code: -Drestassured.http.client=jdk or -Drestassured.http.client=apache5. Another library can provide a client by implementing io.restassured.common.spi.http.HttpClientProvider. REST Assured only uses such a client when you name it, with client("name"), client(ProviderClass.class) or the system property.

A client that you pick but that isn’t on the classpath fails the request with a message that says which dependency to add. If you exclude http-client-jdk and have no other client, the request fails with “REST Assured found no HTTP client module. Add io.rest-assured:http-client-jdk (no other dependencies) or io.rest-assured:http-client-apache5 to your test dependencies.”

The settings of httpClientConfig(), such as connectTimeout(..), readTimeout(..) and expectContinue(..), work with every client (see HTTP Client Config). For anything else, customize the builder of the client’s library:

RestAssured.config = RestAssured.config().httpClient(httpClientConfig()
.customizeClient(java.net.http.HttpClient.Builder.class, builder -> builder.executor(executor)));

The customizers run in the order you add them. A customizer for the builder of another client, such as Apache HttpClient 5’s HttpClientBuilder while the JDK client is used, fails the request with a message, for example “A customizer for org.apache.hc.client5.http.impl.classic.HttpClientBuilder is configured, but REST Assured uses the JDK HTTP client.”

The JDK client is shared by all requests with the same settings, so they share its connections too. connectionConfig().closeIdleConnectionsAfterEachResponse() gives each request a new client instead.

For Apache HttpClient 5, customize its org.apache.hc.client5.http.impl.classic.HttpClientBuilder. The customizers get the builder after REST Assured has set it up:

RestAssured.config = RestAssured.config().httpClient(httpClientConfig()
.customizeClient(HttpClientBuilder.class, builder -> builder.addRequestInterceptorFirst(
(request, entity, context) -> request.addHeader("X-Trace-Id", traceId))));

REST Assured can’t read what a customizer set on the builder, so this is what each setting does:

A customizer that calls Has this effect
The interceptors, such as addRequestInterceptorFirst(..), and setUserAgent(..), setDefaultHeaders(..), setKeepAliveStrategy(..), setConnectionReuseStrategy(..) and evictIdleConnections(..) They apply
setConnectionManager(..) Replaces REST Assured’s connection manager, together with its SSL settings, its connect timeout and its pool, which has no limit. It’s the way to limit the connections, and connectionRequestTimeout(..) then limits the wait for one. closeIdleConnectionsAfterEachResponseAfter(..) doesn’t reach it
setDefaultAuthSchemeRegistry(..) Replaces REST Assured’s registry. Keep "ntlm" (NTLMSchemeFactory.INSTANCE) in it for auth().ntlm(..), and "basic" and "digest" for the login to a proxy
useSystemProperties() HttpClient reads the system properties http.agent, for the User-Agent, and http.keepAlive, for whether connections are kept, and nothing else
Retries, redirects, cookies, decompression, setTargetAuthenticationStrategy(..) and setRoutePlanner(..) No effect. REST Assured switches them back after the customizers, since it does them or decides them itself
setDefaultRequestConfig(..), setDefaultCredentialsProvider(..) and setProxy(..) No effect, since each request has REST Assured’s config, credentials and route. Use httpClientConfig(), auth() and proxy(..) instead

A client shuts its connection manager down when it’s closed, and REST Assured closes the client of a request after its response, unless reuseHttpClientInstance() keeps the client. So create the connection manager inside the customizer, or create it once, call setConnectionManagerShared(true) and close it yourself. A limit that holds across requests needs reuseHttpClientInstance() or such a shared connection manager:

RestAssured.config = RestAssured.config().httpClient(httpClientConfig()
.reuseHttpClientInstance()
.customizeClient(HttpClientBuilder.class, builder -> builder.setConnectionManager(
PoolingHttpClientConnectionManagerBuilder.create().setMaxConnTotal(10).build())));

A request whose connection manager has been shut down fails with an IllegalStateException that says so.

How the JDK client differs from HttpClient 4

Section titled “How the JDK client differs from HttpClient 4”
What HttpClient 4 JDK client
Response header names, in headers(), the logs and assertion messages As the server sent them, in its order In lower case and sorted by name. Assertions on headers ignore the case of the name, so they pass
Status line The server’s reason phrase The standard reason phrase of the status code, such as HTTP/1.1 404 Not Found, whatever the server sent
User-Agent Apache-HttpClient/4.5.14 Java-http-client/<Java version>. Set the header for another
Connections A new connection for each request Requests with the same settings share one client, and reuse its connections. A response whose body is never read keeps its connection until it’s garbage collected. The javax.net.ssl trust store properties are read when that client is created
Read timeout (readTimeout(..)) Each pause while reading Connecting and waiting for the response headers, not reading the body. It’s a ConnectionTimeoutException when it fires before the connection is up
connectionRequestTimeout(..) The wait for a pooled connection No effect, since the JDK client doesn’t limit its connections. A warning is logged once
closeIdleConnectionsAfterEachResponseAfter(..) with an idle time above 0 Closes the connections that were idle for longer after each response No effect, since the JDK client closes idle connections after a time that it decides. A warning is logged once. See jdk.httpclient.keepalive.timeout under JVM properties
Retries Its retry handler The JDK client retries when connecting fails, or when a reused connection turns out to be closed. See JVM properties
Closing a partly read body stream Reads the rest of the body Closes the connection
Several Cookie headers Sent as they are Joined into one
A proxy’s Basic login request (407) Answered once, then the login is sent up front, also after a redirect or a challenge Answered for each request that REST Assured sends, also after a redirect or a challenge. A Proxy-Authorization header is only sent to the proxy
A proxy that refuses an HTTPS tunnel Its response An IOException
OPTIONS and TRACE without a body, and HEAD on Java 17 No Content-Length Content-Length: 0
Expect: 100-continue and a chunked body As written here Expect: 100-Continue and Transfer-encoding: chunked
A host name that a HostnameVerifier rejects SSLPeerUnverifiedException SSLException caused by the SSLPeerUnverifiedException
A body cut short ConnectionClosedException A plain IOException
HTTP version HTTP/1.1 HTTP/1.1

The JDK client can’t:

  • send the Host, Connection or Upgrade headers, unless the JVM runs with -Djdk.httpclient.allowRestrictedHeaders naming them,
  • send an Expect header other than 100-continue,
  • do NTLM authentication (auth().ntlm(..)),
  • use TLS to the proxy (ProxySpecification.withScheme("https")),
  • log in to a proxy with Basic for an HTTPS request, unless the JVM runs with -Djdk.http.auth.tunneling.disabledSchemes= (an empty value),
  • send with an Authenticator set through customizeClient(..).

Those fail the request with an UnsupportedOperationException whose message says what to do, for example “The JDK HTTP client (the default module, http-client-jdk) can’t send a Host header. Add io.rest-assured:http-client-apache5 to your test dependencies and REST Assured uses it, or start the JVM with -Djdk.httpclient.allowRestrictedHeaders=host.” Apache HttpClient 5 can do all of them but the last.

How Apache HttpClient 5 differs from the JDK client

Section titled “How Apache HttpClient 5 differs from the JDK client”
What JDK client Apache HttpClient 5
Response header names, in headers(), the logs and assertion messages In lower case and sorted by name As the server sent them, in its order, as with HttpClient 4
Status line The standard reason phrase of the status code The server’s reason phrase, as with HttpClient 4
User-Agent Java-http-client/<Java version> Apache-HttpClient/<version> (Java/<Java version>). HttpClient 4 sent Apache-HttpClient/4.5.14
Connections Requests with the same settings share one client, and reuse its connections A new client, and so a new connection, for each request, as with HttpClient 4. The client lasts across the redirects of the request and the answers to its login challenges, and is closed after the response. With reuseHttpClientInstance(), the requests share one client, which keeps its connections, with no limit on their number
Read timeout (readTimeout(..)) Connecting and waiting for the response headers, not reading the body Each read, also of the body after the request, as with HttpClient 4
connectionRequestTimeout(..) No effect. A warning is logged once Only has an effect when a customizer sets a connection manager that limits the connections
closeIdleConnectionsAfterEachResponseAfter(..) with an idle time above 0 No effect. A warning is logged once Closes the connections that were idle for longer after each response, as with HttpClient 4, for a client kept with reuseHttpClientInstance(). Without it, the client of each request is closed after its response anyway
Retries When connecting fails, or when a reused connection turns out to be closed None
Closing a partly read body stream Closes the connection Drops the connection, so a client kept with reuseHttpClientInstance() sends the next request on a new connection, as it does after a response whose body is never read. HttpClient 4 read the rest of the body first
Several Cookie headers Joined into one Sent as they are, as with HttpClient 4
A proxy’s Basic login request (407) Answered for each request that REST Assured sends, also after a redirect or a challenge Answered once, then the login is sent with every later request of the client, as with HttpClient 4
A proxy that refuses an HTTPS tunnel An IOException Its response with HttpClient 5.4 and later, as with HttpClient 4. A ClientProtocolException with HttpClient 5.2 and 5.3
OPTIONS and TRACE without a body, and HEAD on Java 17 Content-Length: 0 No Content-Length, as with HttpClient 4
A custom method without a body, such as PURGE Content-Length: 0, as with HttpClient 4 No Content-Length
A host name that a HostnameVerifier rejects SSLException caused by the SSLPeerUnverifiedException SSLPeerUnverifiedException, as with HttpClient 4
A server that rejects a missing client certificate, with TLS 1.3 SSLException SSLException, or a SocketException (broken pipe), depending on timing
HTTP version HTTP/1.1 HTTP/1.1

Apache HttpClient 5 also does what the JDK client can’t, apart from the Authenticator: it sends the Host, Connection and Upgrade headers and any Expect header as given, answers NTLM challenges, connects to a proxy with the scheme https over TLS, with the SSL settings of the request, and logs in to a proxy with Basic for an HTTPS request. NTLM is deprecated in HttpClient 5 and may be removed in HttpClient 6.

Apache HttpClient 5 can’t:

  • send a body with a TRACE request. It fails with an UnsupportedOperationException whose message says what to do, for example “Apache HttpClient 5 (found on the classpath, http-client-apache5) can’t send a body with a TRACE request. Choose another client with httpClientConfig().client(..).”
  • send a TRACE request with an Authorization or Cookie header, with HttpClient 5.4 and later,
  • send an OPTIONS request with a body but without a Content-Type, with HttpClient 5.4 and later (HttpCore 5.3 and later).

The last two fail with HttpClient’s ClientProtocolException, whose cause names the rule, such as “TRACE request MUST NOT contain an Authorization header.” or “OPTIONS request must have Content-Type header”. The JDK client, and HttpClient 4 in REST Assured 6, send all three.

Like the JDK client, it sends HTTP/1.1 only.

Content-Length and Transfer-Encoding headers

Section titled “Content-Length and Transfer-Encoding headers”

REST Assured sends the length of the body itself, as a Content-Length header, or chunked when the length is unknown. A request that sets a Content-Length or Transfer-Encoding header, for example with header("Content-Length", ..), fails with an UnsupportedOperationException, whichever client sends it:

java.lang.UnsupportedOperationException: REST Assured can't send a Content-Length header that the request sets. Remove the header: REST Assured sends the length of the body itself, as a Content-Length header, or chunked when the length is unknown.

Remove the header. No client can send these headers, so changing the client doesn’t help. In REST Assured 6, Apache HttpClient 4 refused them on a request with a body, with a ClientProtocolException (“Content-Length header already present”), but sent them as given on a GET, HEAD, DELETE, OPTIONS or TRACE request without a body. So, for example, given().header("Content-Length", "0").delete(..) worked in 6.x and now fails.

The JDK reads these system properties for the whole JVM, most of them once, so set them where the JVM that runs the tests starts, for example in the argLine of the Maven Surefire plugin or jvmArgs of a Gradle Test task:

Property Effect
jdk.httpclient.allowRestrictedHeaders=host,connection,upgrade Lets requests set the headers it names
jdk.http.auth.tunneling.disabledSchemes= (an empty value) Allows a Basic proxy login for HTTPS requests, which the JDK turns off by default
jdk.httpclient.disableRetryConnect=true Doesn’t retry when connecting fails
jdk.httpclient.enableAllMethodRetry=true Also retries requests that aren’t idempotent, such as POST, on a reused connection that turns out to be closed
jdk.httpclient.keepalive.timeout=<seconds> How long an idle connection is kept

For example with Maven:

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>-Djdk.httpclient.allowRestrictedHeaders=host</argLine>
</configuration>
</plugin>
  • A connect timeout throws io.restassured.common.exception.ConnectionTimeoutException, and a read timeout io.restassured.common.exception.ResponseTimeoutException. With Apache HttpClient 5, a timeout while waiting for a pooled connection throws a ConnectionTimeoutException too, and the read timeout also covers reading the body after the request, such as in asString(). The JDK client supports neither (see the table above). Both exceptions are SocketTimeoutExceptions. Before, HttpClient 4 threw its ConnectTimeoutException or ConnectionPoolTimeoutException, which aren’t, or a plain SocketTimeoutException.
  • A host that can’t be resolved still throws java.net.UnknownHostException. With the JDK client its cause is the client’s ConnectException. With Apache HttpClient 5 it has no cause and the same message as in 6.x.
  • A refused connection throws a java.net.ConnectException: the JDK client’s own, or Apache HttpClient 5’s org.apache.hc.client5.http.HttpHostConnectException.
  • Other errors of Apache HttpClient 5 are its own IOExceptions, such as org.apache.hc.client5.http.ClientProtocolException (see What Apache HttpClient 5 can’t do).
  • More redirects than redirectConfig().maxRedirects(..) allows throw io.restassured.common.exception.TooManyRedirectsException, and a circular redirect, a relative Location rejected by rejectRelativeRedirect(true), an invalid Location and a redirect without a Location throw java.net.ProtocolException, with the same messages as before. Before, they were HttpClient 4’s ClientProtocolExceptions. TooManyRedirectsException is a ProtocolException.
  • io.restassured.internal.http.HttpResponseException and ResponseParseException extend java.io.IOException instead of HttpClient 4’s HttpResponseException. HttpResponseException.getResponse() and the constructors that took an HttpResponseDecorator are removed.

Code that catches IOException keeps working.

These APIs were deprecated in 6.3.0 (see Preparing for REST Assured 7.0) and are removed:

Removed Use instead
SSLConfig.sslSocketFactory(..), getSSLSocketFactory(), x509HostnameVerifier(..) and getX509HostnameVerifier() sslContext(..), getSSLContext(), hostnameVerifier(..) and getHostnameVerifier()
CertificateAuthSettings.sslSocketFactory(..), x509HostnameVerifier(..) and their getters sslContext(..), hostnameVerifier(..) and their getters
CertAuthScheme.setSslSocketFactory(..), setX509HostnameVerifier(..) and their getters setSslContext(..), setHostnameVerifier(..) and their getters
HttpClientConfig.httpMultipartMode(..) and httpMultipartMode() multipartMode(..) and multipartMode()
HttpClientConfig.HttpClientFactory, httpClientFactory(..), httpClientInstance() and usesDefaultHttpClientFactory() The settings of httpClientConfig(), or customizeClient(..). redirectConfig().followRedirectsForAllMethods(true) instead of LaxRedirectStrategy, and keepResponseCookies(true) instead of the client’s cookie store
QueryableRequestSpecification.getHttpClient() Nothing
CookieFilter.getCookieStore() and extractEffectiveUriFromContext(..) getCookies() and addCookie(..)
authenticate(HTTPBuilder) of the authentication schemes, and io.restassured.internal.http.HTTPBuilder authenticate(AuthenticationContext)

SSLConfig.port(..) and CertificateAuthSettings.port(..), with their getPort(), CertAuthScheme.getPort() and setPort(..), and the port argument of AuthenticationContext.certificate(..) are removed too, without a deprecation first, because they never had an effect: an https URL without a port always went to port 443. Set the port in the URI, such as https://localhost:8443, or with port(..). The OpenRewrite recipe removes the calls.

The root path methods deprecated since REST Assured 4.0.0 are removed too:

Removed Use instead
root(..) rootPath(..)
noRoot() noRootPath()
appendRoot(..) appendRootPath(..)
detachRoot(..) detachRootPath(..)
JsonPath.setRoot(..) and XmlPath.setRoot(..) setRootPath(..)

The first four are methods of then(), expect() and ResponseSpecification, and of the validatable responses of RestAssuredMockMvc and RestAssuredWebTestClient. expect().appendRoot(path) detached the path instead of appending it, so code that used it now gets what appendRootPath(path) does. The OpenRewrite recipe renames the calls.

AuthenticationScheme now has the single method authenticate(AuthenticationContext), so a lambda can be one:

RestAssured.authentication = context -> context.header("X-Api-Key", apiKey);

ContextAuthenticationScheme adds nothing to it any more and is deprecated. It’s kept until REST Assured 8.0.0, so that code written for 6.3.0 still compiles, and the OpenRewrite recipe replaces it with AuthenticationScheme. A class that extends one of REST Assured’s schemes and only overrides authenticate(HTTPBuilder) must override authenticate(AuthenticationContext) instead. A scheme compiled against REST Assured 6 that doesn’t implement authenticate(AuthenticationContext), for example from a shared test jar, fails the request with an IllegalStateException that names its class.

The OpenRewrite recipe changes the SSL and multipart methods and the root path methods, replaces ContextAuthenticationScheme, and marks the authentication schemes to change.

The HttpClient 4 parameters set with httpClientConfig().setParam(..) and the other deprecated parameter methods still work for the keys that have a replacement, listed in Preparing for REST Assured 7.0, until 8.0.0. Any other key fails the request with an IllegalArgumentException that lists the settings to use instead, since REST Assured would otherwise drop it silently. The four redirect parameters, such as http.protocol.handle-redirects, have never had an effect, since the RedirectConfig outranks them, and log a warning once.

The rest-assured bundle requires the http-client-jdk bundle (io.rest-assured:http-client-jdk), and no longer needs the HttpClient 4 bundles (httpclient, httpmime and httpcore). It imports http-client-apache5 as optional, also dynamically, so that bundle is found when it’s installed. The clients of other libraries are found with ServiceLoader, which in OSGi needs a ServiceLoader Mediator, such as Apache Aries SPI Fly.

http-client-apache5 is a bundle. HttpClient 5’s jars aren’t, so install them wrapped, for example with Pax URL’s wrap: or with bnd: httpclient5, httpcore5 and httpcore5-h2. They import each other and SLF4J, and what HttpClient 5 uses only when it’s there, such as Conscrypt and Brotli, should be imported as optional. Also install slf4j-api and an SLF4J provider, such as slf4j-simple, and with SLF4J 2 an OSGi ServiceLoader Mediator, such as SPI Fly. Without them, the http-client-apache5 bundle doesn’t resolve, and REST Assured uses the JDK client.

The static settings of RestAssured are shared by all threads. Tests that run in parallel and need different settings, such as a port per Spring Boot test class, can now bind a client to their thread, and keep plain given() in the test bodies:

@BeforeEach
void useOwnPort() {
RestAssured.useClient(RestAssuredClient.builder().port(port).build());
}
@AfterEach
void unbindClient() {
RestAssured.resetClient();
}
@Test
void shows_open_orders() {
given().queryParam("status", "open").when().get("/orders").then().statusCode(200);
}

RestAssured.reset() also unbinds the client, but it resets the static settings for every thread, so in a parallel suite use resetClient(). given(client) names a client for one request, for example for a second service. A client named in given(client) wins over the one bound to the thread, which wins over the static settings. The binding isn’t inherited by other threads, so a condition that Awaitility polls on its own thread uses the static settings unless it names the client with given(client) or Awaitility polls with pollInSameThread().

A client is an immutable set of settings, built with RestAssuredClient.builder(), whose methods are named like the static fields and setters without set, or from the current static settings with RestAssured.client(). toBuilder() gives a builder for a changed copy. A client owns no connections or cookies and has no close(): its requests get their HTTP client by the same rules as those of the static API.

Without a client, everything works as before. See Parallel tests and RestAssuredClient in the usage guide, and Parallel tests with RestAssuredMockMvc for Spring.

Specifications no longer change when sent or validated

Section titled “Specifications no longer change when sent or validated”

Sending a request, and validating a response with then().spec(..) or validate(..), now works on copies of the request and response specification, so the specifications you hold never change. One specification can be sent, or validate responses, many times and from many threads, and sends the same request each time (#945). Before, a specification sent twice sent the query parameters of the path twice, and one used from many threads mixed up the requests. The filter instances on a specification are still shared by every send, so a filter used from many threads must be thread safe.

Since the specification doesn’t change, you can no longer read back from it what the send set. SpecificationQuerier.query(spec) shows what was set before the send: no method, path or path parameters, no query parameters from the path’s query string, and no default headers, HTTP client, config or anything a filter added, such as a cookie from CookieFilter or the session id from SessionFilter. Filters and the failure listeners of FailureConfig get the copies, with everything set during the send. given(requestSpecification, responseSpecification) no longer links the two specifications, so afterwards, as for any built specification, requestSpecification.get(..) and responseSpecification.when() throw an IllegalStateException.

The config is fixed along the way:

  • given(requestSpecification, responseSpecification) validates with the config of the request specification, then RestAssured.config, instead of RestAssured.config as it was when the response specification was built. A config set on RestAssured.requestSpecification applies to expect() assertions too.
  • then().spec(responseSpecification) no longer leaves the config of the response in the specification, so a later given(requestSpecification, responseSpecification) doesn’t validate with it, and a failed validation logs its own request and response, not those of another thread.

In 7.0.0, a request gets the static settings, or those of its client, where it’s used, and not where a specification was built. This section shows what changes, each with an example.

Spec builders no longer copy the static settings

Section titled “Spec builders no longer copy the static settings”

RequestSpecBuilder and ResponseSpecBuilder no longer copy the static settings of RestAssured when they’re created, such as baseURI, port, basePath, authentication, config, filters(..), proxy, requestSpecification, rootPath, the registered parsers and defaultParser. The static settings, or those of a client, are applied when the specification is used, with given(..), given().spec(..), expect().spec(..) or then().spec(..), as they are at that time, and what was set on the builder is applied on top (#1977).

RestAssured.requestSpecification = new RequestSpecBuilder().addHeader("X-Tenant", "a").build();
RequestSpecification spec = new RequestSpecBuilder().addQueryParam("q", "x").build();
given(spec).get("/search");
// Before: X-Tenant was sent twice, once from the copy in spec
// Now: X-Tenant is sent once

A change of RestAssured.config, filters(..), rootPath or the registered parsers after the builder was created now reaches the request and its validation, and a specification used with a client gets the settings of the client. SpecificationQuerier.query(spec) shows only what was set on the builder:

RestAssured.baseURI = "http://example.com";
SpecificationQuerier.query(new RequestSpecBuilder().build()).getBaseUri();
// Before: http://example.com
// Now: http://localhost

To keep a static setting with a specification, set it on the builder, for example new RequestSpecBuilder().setConfig(RestAssured.config()) or new ResponseSpecBuilder().rootPath(RestAssured.rootPath).

Some builder methods compute something right away, and they now use the config of the builder, the default one unless setConfig(..) sets another, instead of the static config: the body that setBody(..) serializes, the session id name of setSessionId(value), the update strategy of addParam(..), addQueryParam(..) and addFormParam(..), the default control name and file name of addMultiPart(..), and which headers addHeader(..) overwrites. They aren’t redone when the specification is used.

RestAssured.config = RestAssured.config().sessionConfig(sessionConfig().sessionIdName("phpsessionid"));
new RequestSpecBuilder().setSessionId("1234").build();
// Before: sends the cookie phpsessionid=1234
// Now: sends JSESSIONID=1234
new RequestSpecBuilder().setConfig(RestAssured.config()).setSessionId("1234").build();
// Now: sends phpsessionid=1234

log(..) on a builder is the exception: it takes its settings from the config of the request when it logs, see Logging.

MockMvcRequestSpecBuilder and WebTestClientRequestSpecBuilder no longer copy the static settings of RestAssuredMockMvc and RestAssuredWebTestClient either, such as the MockMvc or WebTestClient instance, basePath, config, requestSpecification, responseSpecification, authentication, resultHandlers(..) and postProcessors(..). The same rules as for RequestSpecBuilder apply, with setConfig(RestAssuredMockMvc.config()) or setConfig(RestAssuredWebTestClient.config()) to compute with the static config.

RestAssuredMockMvc.mockMvc(mockMvc);
RestAssuredMockMvc.resultHandlers(print());
MockMvcRequestSpecification spec = new MockMvcRequestSpecBuilder().addQueryParam("name", "Johan").build();
given().spec(spec).get("/greeting");
// Before: print() ran twice
// Now: print() runs once
spec.get("/greeting");
// Before: used the static MockMvc instance
// Now: fails with "You haven't configured a MockMVC instance"

A specification sent on its own needs its own instance: new MockMvcRequestSpecBuilder().setMockMvc(mockMvc), or setStandaloneSetup(..) or setWebAppContextSetup(..), and setWebTestClient(..) for WebTestClient. A MockMvc or WebTestClient instance, or a base path, set statically after the builder was created now reaches the request.

given(requestSpecification, responseSpecification)

Section titled “given(requestSpecification, responseSpecification)”

given(requestSpecification, responseSpecification) now merges both specifications into the static settings, or those of the client bound with RestAssured.useClient(..), like given().spec(requestSpecification).expect().spec(responseSpecification), instead of sending them as they were built. The static settings are read when given(..) is called, and RestAssured.responseSpecification now applies too.

RequestSpecification request = new RequestSpecBuilder().addQueryParam("q", "x").build();
ResponseSpecification response = new ResponseSpecBuilder().expectContentType(ContentType.JSON).build();
RestAssured.port = 8081;
RestAssured.responseSpecification = new ResponseSpecBuilder().expectStatusCode(200).build();
given(request, response).get("/search");
// Before: sent to port 8080, the port when the builder was created, and the status code wasn't checked
// Now: sent to port 8081, and the status code must be 200

Merging a response specification, with expect().spec(..), then().spec(..), ResponseSpecBuilder.addResponseSpecification(..), given(requestSpecification, responseSpecification) or RestAssured.responseSpecification, now keeps the status code, status line, content type, response time, root path, default parser and log detail that the merged specification doesn’t set. Before, they were cleared.

ResponseSpecification json = new ResponseSpecBuilder().expectContentType(ContentType.JSON).build();
expect().statusCode(200).spec(json).when().get("/orders");
// Before: the status code wasn't checked, since json expects none
// Now: the status code must be 200

Body expectations that the merged specification defined without a root path now take the root path in effect where they’re merged, such as RestAssured.rootPath or one set earlier in the chain:

ResponseSpecification common = new ResponseSpecBuilder().expectBody("status", equalTo("ok")).build();
get("/orders/42").then().rootPath("data").spec(common);
// Before: checked status
// Now: checks data.status

To check status as before, merge common before you set the root path: then().spec(common).rootPath("data").

Merging specifications, for example with given().spec(..), given(requestSpecification, responseSpecification) or RequestSpecBuilder.addRequestSpecification(..), no longer adds a header or cookie that’s already there with the same value, or the same multipart part, again. In the Spring modules, that includes the same result handler or post processor.

RequestSpecification a = new RequestSpecBuilder().addHeader("X", "1").build();
RequestSpecification b = new RequestSpecBuilder().addHeader("X", "1").build();
given().spec(a).spec(b).get("/orders");
// Before: X: 1 was sent twice
// Now: X: 1 is sent once

So the static headers, cookies and multipart parts are also sent once when the merged specification was made with given() or with(), as in given(with().header("X", "1"), expect().statusCode(200)). A header with the same name and another value is still added, and a header that HeaderConfig overwrites, such as Content-Type or Accept, still replaces the current one.

A relative path of the page with the CSRF token, set with given().csrf(..) or CsrfConfig.csrfTokenPath(..), is now sent to the base URI, port and base path of the request, also with form authentication, instead of to RestAssured.baseURI, port and basePath.

given().baseUri("http://localhost:8081").csrf("/users/form").formParam("firstName", "John").when().post("/users");
// Before: fetched the token from http://localhost:8080/users/form
// Now: fetches it from http://localhost:8081/users/form

For a request sent to a full URL, such as post("http://localhost:8081/users"), give the page with the CSRF token as a full URL too. The request for that page now also uses the config and the proxy of the request, so relaxedHTTPSValidation() or a proxy set on the request applies to it, and the extra requests of form authentication and CSRF use the client of the request.

RestAssured.certificate(certURL, password) now takes the SSL settings (the key store and trust store instances and types, the key store password, the host name verifier, the SSLContext and relaxed HTTPS validation) from the SSLConfig of the request when the request is sent: that of given().config(..) if it sets one, else that of the client of the request, else RestAssured.config.

RestAssured.authentication = certificate("/client.p12", "secret");
RestAssured.config = RestAssured.config().sslConfig(sslConfig().keystoreType("PKCS12"));
get("https://localhost:8443/secured");
// Before: certificate(..) took the SSL settings of RestAssured.config when it was called, so the key store type was ignored
// Now: the key store type is used

The scheme is still a CertAuthScheme, but getKeystoreType(), getTrustStoreType(), getKeyStore(), getTrustStore(), getHostnameVerifier() and getSslContext() no longer show the settings that are used, and its setters don’t change what is sent. given().auth().certificate(certURL, password) and the certificate(..) methods that take a CertificateAuthSettings are unchanged.

The logging filter of log(), as in given().log().headers(), and of RequestSpecBuilder.log(..), MockMvcRequestSpecBuilder.log(..) and WebTestClientRequestSpecBuilder.log(..), now takes the blacklisted headers, the print stream, the URL encoding of the request URI and pretty printing from the config of the request when it logs:

given().
log().headers().
config(RestAssured.config().logConfig(logConfig().blacklistHeader("Authorization"))).
header("Authorization", "Bearer secret").
when().
get("/orders");
// Before: printed Authorization=Bearer secret, since log() read the config as it was when log() was called
// Now: prints Authorization=[ BLACKLISTED ]

Pretty printing given explicitly, as in log().body(false), still wins.

RequestLoggingFilter, ResponseLoggingFilter and ErrorLoggingFilter created without a pretty-print argument now pretty print the body if the log config of the request has pretty printing enabled when they log:

RestAssured.config = RestAssured.config().logConfig(logConfig().enablePrettyPrinting(false));
given().filter(new RequestLoggingFilter()).body("{\"a\":1}").post("/orders");
// Before: RequestLoggingFilter always pretty printed the body
// Now: prints {"a":1} as it is

Before, ResponseLoggingFilter and ErrorLoggingFilter took the setting of RestAssured.config when they were created. expect().log().ifError(), ifStatusCodeIsEqualTo(..) and ifStatusCodeMatches(..) follow the log config of the request too.

The response log now hides the headers blacklisted with LogConfig.blacklistHeader(..) or blacklistDefaultSensitiveHeaders() too. Before, only then().log() hid them, so expect().log(), ResponseSpecBuilder.log(..) and logging if validation fails printed their values, for example to the log of a CI build. These, the response logs of form authentication and CSRF, and a ResponseLoggingFilter or ErrorLoggingFilter created without blacklisted headers now take them from the log config of the request when they log:

RestAssured.config = RestAssured.config().logConfig(logConfig().blacklistHeader("X-Session-Token"));
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
get("/login").then().statusCode(200);
// Before: when the status code wasn't 200, the response log printed the value of X-Session-Token
// Now: it prints [ BLACKLISTED ] instead

Blacklisted headers that you give to the constructor of a logging filter are matched ignoring case, as in LogConfig, so they’re hidden also when the HTTP client gives the header names in lower case, as the JDK client does.

json-schema-validator now uses the networknt json-schema-validator 3.0.9 (was 2.0.8). It’s built on Jackson 3 (tools.jackson), so the module brings Jackson 3, and no longer Jackson 2 or Guava. The java-json-tools json-schema-validator, which REST Assured used for draft-03 and draft-04, is now optional. Which validator validates a schema:

Schema Validated by
Draft-06, draft-07, 2019-09 and 2020-12 The networknt json-schema-validator
Draft-04, and a schema without $schema, unless schemaVersion(..) picks another draft The java-json-tools json-schema-validator if it’s on the classpath, exactly as in REST Assured 6. Otherwise the networknt json-schema-validator, as draft-04
Draft-03 The java-json-tools json-schema-validator only

Add the java-json-tools json-schema-validator to your test dependencies to validate draft-03 schemas, to use a JsonSchemaFactory or checked validation, or to keep exactly the behavior of REST Assured 6 for draft-04 and schemas without $schema:

<dependency>
<groupId>com.github.java-json-tools</groupId>
<artifactId>json-schema-validator</artifactId>
<version>2.2.14</version>
<scope>test</scope>
</dependency>

Gradle:

testImplementation("com.github.java-json-tools:json-schema-validator:2.2.14")

Without it, a draft-03 schema fails with a JsonSchemaValidationException whose message says which dependency to add, and so does settings().jsonSchemaFactory(). Code that uses JsonSchemaFactory needs it to compile, and checkedValidation(..) has no effect without it. The two validators word their messages differently, so a test that checks the message of a failed validation may need a change.

REST Assured prefers Jackson 3 for object mapping when it’s on the classpath, for example in as(Greeting.class) and body(greeting). So with json-schema-validator on the classpath, objects are now serialized and deserialized with Jackson 3, also when your project uses Jackson 2 and configures it with jackson2ObjectMapperFactory(..). Jackson 3 still reads the com.fasterxml.jackson.annotation annotations, but not the modules and settings of a Jackson 2 ObjectMapper, and has other defaults. To keep Jackson 2:

RestAssured.config = RestAssured.config().objectMapperConfig(objectMapperConfig().defaultObjectMapperType(ObjectMapperType.JACKSON_2));

The same goes for RestAssuredMockMvc.config and RestAssuredWebTestClient.config. For one request, use as(Greeting.class, ObjectMapperType.JACKSON_2) or body(greeting, ObjectMapperType.JACKSON_2).

JsonPath (json-path-gpath) now prefers Jackson 3 over Jackson 2 when both are on the classpath and nothing else is configured, like REST Assured’s object mapping. So response.jsonPath().getObject(..) and response.as(..) deserialize with the same Jackson. Before, getObject(..) used Jackson 2 and as(..) Jackson 3. Without Jackson, JsonPath uses the first of Gson, Johnzon and JSON-B (Yasson) on the classpath, as before.

Lotto lotto = get("/lotto").jsonPath().getObject("lotto", Lotto.class);
// Before: deserialized with Jackson 2 when both Jacksons are on the classpath
// Now: deserialized with Jackson 3, the same Jackson that as(..) uses

response.jsonPath() now also follows ObjectMapperConfig.defaultObjectMapperType(..) of the request, except JAXB and JAKARTA_EE, which only map XML, and uses the JSON-B factory of the ObjectMapperConfig, like its Jackson and Gson factories. Before, it ignored them. To keep Jackson 2:

// For response.jsonPath(), and as(..) too
RestAssured.config = RestAssured.config().objectMapperConfig(objectMapperConfig().defaultObjectMapperType(ObjectMapperType.JACKSON_2));
// For the JsonPath instances that you create, such as JsonPath.from(json)
JsonPath.config = new JsonPathConfig().defaultParserType(JsonParserType.JACKSON_2);

auth().oauth(..) and RestAssured.oauth(..) now sign the request themselves, with HMAC-SHA1 as RFC 5849 describes, so OAuth 1 no longer needs scribejava. If you added com.github.scribejava:scribejava-apis to your build for REST Assured, remove it. A scribejava jar that’s still on the classpath isn’t used.

The Authorization header and the query string are written as with scribejava 2.5.3. Where scribejava didn’t follow RFC 5849, REST Assured now does, so a server that follows the RFC can verify these signatures:

  • The parameters are sorted by their encoded names and values. scribejava sorted the decoded ones, which gives another order for names or values with characters other than letters, digits, -, ., _ and ~, such as non-ASCII names.
  • A query parameter value that contains = is signed whole. scribejava signed only the part of the value before its first =.
  • In the signed URI, the scheme and host are in lower case, an empty path is /, and user info and a fragment are left out.
  • An empty consumer secret is allowed. scribejava failed with an OAuthSignatureException.
  • REST Assured no longer includes a path engine or Groovy, and json-path and xml-path are renamed. See Upgrading to 7.0.
  • The ObjectMapperConfig factory getters and JsonConfig.numberReturnType() return the rest-assured-common types, and Jackson 1 is removed. See Code that no longer compiles.
  • REST Assured checks that rest-assured-common, json-path-gpath and xml-path-gpath have the same major and minor version as rest-assured.
  • A JSON path that starts with $. or $[, as in Jayway JsonPath, now starts at the root, like $ alone. For example, $.store.book[0].title is the same as store.book[0].title. Before, such a path gave null on an object. Keys that start with $, such as $schema, are unchanged, and $.. is still not supported.
  • When a Map or Collection request body is written as JSON as it is, which happens when the ObjectMapper given to body(object, mapper) returns one, the values that JSON has no type for, such as beans, records, java.time values, Locale and File, are now written by the configured object mapper, such as Jackson or Gson, instead of as an object of their bean properties. A URI is written as a string, like a URL, and an Optional as its value, or null when it’s empty. A NaN or infinite number fails with an IllegalArgumentException.
  • A cookie without value, such as given().cookie("a"), is sent as a= instead of a=null.
  • A Cookie header set with header("Cookie", ..) is no longer dropped when the request also has cookies. Both are sent in one Cookie header.
  • Headers whose names differ only in case, such as X-Multi and x-multi, are both sent, under the name used first. Before, only the last one was sent.
  • auth().digest(..) only answers a Digest challenge. Before, it also sent the password in clear text to a server that asked for Basic authentication. Use basic(..) to answer both.
  • The trustStore(path, password) and keyStore(path, password) shortcuts no longer allow all host names. Add allowAllHostnames() to the SSL config to get the old behavior back.
  • multiPart("name", bytes) with a byte[] sends the bytes as they are, as application/octet-stream, instead of as Base64 in a text/plain part.
  • A String part without a charset is encoded in the charset of the multipart request (UTF-8 unless configured otherwise) instead of US-ASCII.
  • Sending a request no longer changes the specification it was sent from, so you can’t read back from it what the send set. See Specifications no longer change when sent or validated.
  • The JDK’s HTTP client sends the requests instead of Apache HttpClient 4, which is no longer a dependency, nor are httpmime, httpcore, commons-codec and commons-logging. See How the JDK client differs from HttpClient 4.
  • The APIs that take or return HttpClient 4 types, and HTTPBuilder, are removed. See Removed APIs.
  • The SSL port settings, sslConfig().port(..) and certAuthSettings().port(..), are removed. See Removed APIs.
  • Timeouts, redirect errors, HttpResponseException and ResponseParseException are no longer HttpClient 4 exceptions. See Exceptions.
  • An HttpClient 4 parameter without a replacement fails the request. See HttpClient 4 parameters.
  • In OSGi, the rest-assured bundle requires the http-client-jdk bundle. See OSGi and the HTTP client.
  • A request that sets a Content-Length or Transfer-Encoding header fails, whichever client sends it. See Content-Length and Transfer-Encoding headers.
  • RequestSpecBuilder and ResponseSpecBuilder no longer copy the static settings, and what a builder computes while building uses its own config. See Spec builders no longer copy the static settings and What a builder computes while building.
  • MockMvcRequestSpecBuilder and WebTestClientRequestSpecBuilder no longer copy the static settings, so a specification sent on its own needs its own MockMvc or WebTestClient instance. See The Spring specification builders.
  • given(requestSpecification, responseSpecification) merges the specifications into the static settings, or those of the bound client, and applies RestAssured.responseSpecification. See given(requestSpecification, responseSpecification).
  • Merging a response specification keeps the expectations that it doesn’t set, and its body expectations take the root path where they’re merged. See Merging a response specification.
  • Merging specifications no longer adds a header or cookie that’s already there with the same value, or the same multipart part, result handler or post processor, again. See Equal headers and cookies are added once.
  • A relative path of the page with the CSRF token is sent to the base URI, port and base path of the request. See The page with the CSRF token.
  • RestAssured.certificate(certURL, password) takes the SSL settings of the request when it’s sent. See certificate(certURL, password).
  • The logging filters follow the log config of the request when they log. See Logging.
  • The root path methods deprecated since REST Assured 4.0.0, such as root(..), appendRoot(..) and JsonPath.setRoot(..), are removed. Use rootPath(..) and the other root path methods. See Removed APIs.
  • json-schema-validator uses the networknt json-schema-validator 3.0.9, which brings Jackson 3 instead of Jackson 2 and Guava. REST Assured prefers Jackson 3 for object mapping when it’s on the classpath, so if you use Jackson 2, set objectMapperConfig().defaultObjectMapperType(ObjectMapperType.JACKSON_2). See JSON Schema validation and Jackson 3.
  • The java-json-tools json-schema-validator is optional in json-schema-validator. Add com.github.java-json-tools:json-schema-validator:2.2.14 to validate draft-03 schemas, to use a JsonSchemaFactory or checked validation, or to keep exactly the behavior of REST Assured 6 for draft-04 and schemas without $schema. Without it, those are validated as draft-04 by the networknt json-schema-validator. See JSON Schema validation and Jackson 3.
  • JsonPath prefers Jackson 3 over Jackson 2 when both are on the classpath, and response.jsonPath() follows defaultObjectMapperType(..) of the request. See JsonPath prefers Jackson 3.
  • ResponseSpecBuilder.setConfig(RestAssuredConfig), whose config the response specification validates with. A LogConfig with enableLoggingOfRequestAndResponseIfValidationFails() in it logs the request and the response when validation fails, or only the response with then().spec(..), since the request was sent before.
  • The OpenRewrite recipe io.restassured.UpgradeToRestAssured7, see Let OpenRewrite do it. It also replaces ContextAuthenticationScheme, which is deprecated and removed in REST Assured 8.0.0, with AuthenticationScheme, which takes the same lambda.
  • httpClientConfig().client(..), the system property restassured.http.client and customizeClient(..), see The HTTP client.
  • The http-client-apache5 module, which sends the requests with Apache HttpClient 5, see Apache HttpClient 5. With it, connectionConfig().closeIdleConnectionsAfterEachResponseAfter(..) closes the idle connections of a client kept with reuseHttpClientInstance(), as in 6.x.
  • Basic and Digest authentication, OAuth, redirects and the kept cookies of keepResponseCookies(true) are handled by REST Assured itself and work the same with every client. A byte[] body is sent again when it answers a challenge or follows a 307 or 308 redirect.
  • Form authentication uses the first text input and the first password input of a login page that has several, and finds them whatever the case of their type.
  • Deserializing an XML body to a generic type with the JAXB or Jakarta EE object mapper fails with an IllegalArgumentException that says JAXB can only deserialize to a class, instead of Groovy’s MissingMethodException.
  • RestAssuredClient, RestAssured.given(client), useClient(..), resetClient() and client(), the same for RestAssuredMockMvc and RestAssuredWebTestClient with RestAssuredMockMvcClient and RestAssuredWebTestClientClient, and Given(client) in the Kotlin and Scala extensions. See Clients for parallel tests.
  • Sending a RestAssuredMockMvc or RestAssuredWebTestClient specification no longer changes it. Each send gets its own logs, so with enableLoggingOfRequestAndResponseIfValidationFails() a specification sent several times, or from many threads, no longer logs the requests of the other sends.
  • The request for the page with the CSRF token of given().csrf(..) and CsrfConfig.csrfTokenPath(..) uses the config and the proxy of the request, as with form authentication.
  • JsonPath.config, XmlPath.config and JsonSchemaValidator.settings are standalone settings: they’re never taken from a request or a client, and using(..) sets them for one JsonPath, XmlPath or matcher.
  • Hamcrest 3.0 (was 2.2), which has the same API. In OSGi, the rest-assured bundle accepts Hamcrest 2.2 and 3.x. Hamcrest 3.0’s own bundle imports org.hamcrest without a version, so if an older Hamcrest bundle is installed too, such as the 1.3 bundle that pax-exam’s JUnit 4 support uses, install Hamcrest 3.0 first, at a lower start level. Otherwise it binds to the older package and the rest-assured bundle doesn’t resolve.
  • json-path-jayway depends on Jayway JsonPath 3.0.0 (was 2.10.0), which needs Java 17. It still works with Jayway JsonPath 2.8.0 and later, so a version that your build manages, such as Spring Boot 4’s 2.10.0, keeps working. In OSGi, Jayway JsonPath 3.0.0 requires the Jackson 3 bundles, see OSGi.
  • json-path-gpath, xml-path-gpath and rest-assured-all depend on Groovy 5.1.3 (was 5.0.8) and work with Groovy 4.0, 5.x and 6.x. In OSGi, they import Groovy 4.0 up to, but not including, 7.0, instead of 5.x only. See Groovy versions.
  • scala-extensions depends on scala3-library_3 3.3.9 (was 3.3.8), the latest Scala 3.3 LTS. It keeps working with every newer Scala 3, and CI also tests it with the latest Scala 3 (3.10.0).
  • The optional dependencies that REST Assured is compiled against, which you bring yourself, are upgraded: Jackson 2 (jackson-databind, jackson-core and jackson-module-kotlin) to 2.22.4, Jackson 3 (tools.jackson) to 3.2.4, Gson to 2.15.0, Jakarta XML Binding to 4 (jakarta.xml.bind-api 4.0.5 and jaxb-runtime 4.0.9, was 3.0.1 and 3.0.2), Spring Framework in spring-mock-mvc, spring-web-test-client and their Kotlin extensions to 7.0.10, Spring Security in spring-mock-mvc to 7.1.2 (was 6.5.11), and the Jakarta EE Web API in spring-web-test-client-kotlin-extensions to 11.0.0. In OSGi, the optional imports of jakarta.xml.bind accept Jakarta XML Binding 3.x and 4.x (was 3.x only), and the optional import of com.google.gson accepts Gson 2.15 or later (was 2.14 or later).
  • The OSGi bundles now require a Java 17 execution environment (osgi.ee JavaSE 17), which every Java 17 framework provides. See OSGi.
  • spring-web-test-client no longer depends on org.synchronoss.cloud:nio-multipart-parser, which Spring Framework 6 and later don’t use, so it, nio-stream-storage and slf4j-api 1.7.12 leave your classpath.
  • OAuth 1 (auth().oauth(..)) no longer needs scribejava, and in OSGi the rest-assured bundle no longer imports com.github.scribejava. Signatures now follow RFC 5849 where scribejava didn’t. See OAuth 1 without scribejava.
  • A config set on a request specification, such as new RequestSpecBuilder().setConfig(..), now also applies to expect() assertions, and given(requestSpecification, responseSpecification) with logging if validation fails logs the request too, not only the response.
  • Every response log hides the blacklisted headers, not only then().log(). See Logging. LogConfig.blacklistDefaultSensitiveHeaders() no longer adds Authorization, Proxy-Authorization and Cookie to the blacklist of the LogConfig it’s called on, only to the new LogConfig that it returns.
  • Fixes for auth().oauth2(accessToken, OAuthSignature.QUERY_STRING), closeIdleConnectionsAfterEachResponse() with large bodies, dontCloseIdleConnectionsAfterEachResponse(), reuseHttpClientInstance() after a response whose body was never read, RestAssured.keyStore(password), ProxySpecification.withScheme(..), multipart requests in RestAssuredWebTestClient, and pretty printing of a JSON string with an escaped backslash followed by u.

See the change log for all changes.