Skip to content

REST Assured 6.2.0

  • The rest-assured module is now written entirely in Java and no longer depends on Groovy (org.apache.groovy:groovy and groovy-xml) directly. Groovy still comes transitively through json-path and xml-path, which use it for GPath, so the dependencies on your classpath are unchanged. The rest-assured and rest-assured-common OSGi bundles no longer import the Groovy packages, and rest-assured-common no longer depends on Groovy at all.

  • Fixes for tests that run in parallel. Changing the static configuration (for example RestAssured.filters(..), replaceFiltersWith(..), registerParser(..) or unregisterParser(..)) while another thread calls given() could fail with a ConcurrentModificationException or lose filters. given() no longer changes any shared state, and the public static fields of RestAssured, RestAssuredMockMvc and RestAssuredWebTestClient, as well as JsonPath.config, XmlPath.config and JsonSchemaValidator.settings, are now volatile, so a value set before the tests start is seen by every thread. HttpClientConfig.reuseHttpClientInstance() no longer creates more than one HttpClient when several threads send their first request at the same time. The javadoc of RestAssured, RestAssuredMockMvc and RestAssuredWebTestClient now explains how to use the static configuration with parallel tests. In short:

    • Set the static defaults once, before the tests start running in parallel (for example in a @BeforeAll method), and don’t change them while requests are being made.
    • Put values that differ between tests in the request specification instead, for example given().baseUri("http://localhost").port(8081) or given(spec).
    • Don’t execute the same request specification from several threads at once. Start a new one with given() in each thread.
    • Filters added with RestAssured.filters(..) are shared by all requests and must be thread safe.
    • The reused HttpClient from reuseHttpClientInstance() is reconfigured by every request, so it still must not be used by requests that run at the same time.
  • auth().certificate(certURL, password) and RestAssured.certificate(certURL, password) now send the client certificate in the file to servers that ask for one (mutual TLS), as documented. Before, the file was only used as trust store. In the same way, a key store configured with keyStore(..) but without a trust store is now used, where it used to be ignored. Both are non-backward compatible, see SSL and certificates.

    given().auth().certificate("/client-keystore.jks", "password").when().get("https://localhost:8443/secured").then().statusCode(200);
  • JsonPath and JSON body assertions can read a key that contains a colon with the dot notation. This used to fail with “Invalid JSON expression”:

    get("/status").then().body("message.ErrorCode:", equalTo(0));

    A colon in a ternary (a ? b : c) or Elvis (a ?: b) expression, in a closure or in a quoted string is left as is.

  • Request logging no longer prints the content of a multipart File whose mime type isn’t text, such as application/octet-stream (the default) or image/png. Since 5.5.0 the whole file was dumped to the log. Such a part is now logged as the path of the file, as before 5.5.0. A file with a text mime type (text/*, JSON, XML or HTML) is still logged, and is now read with the charset of the part (or of its mime type), or UTF-8 if none is given.

  • Clearer errors. Many failures that used to surface as Groovy exceptions (groovy.lang.MissingMethodException, GroovyRuntimeException, GroovyCastException), a NullPointerException or a StackOverflowError now either work or fail with an exception that says what is wrong. For example:

    • Sending a request directly from a specification built by RequestSpecBuilder.build() fails with an IllegalStateException that tells you to use given().spec(spec) or given(spec).
    • Sending a request directly from a specification built by ResponseSpecBuilder.build() fails with an IllegalStateException that tells you how to add it to a request.
    • A failed body path assertion on a body that the path can’t navigate shows the response body instead of the literal text $contentParser.
    • Form authentication names the additional field that isn’t on the login page, and says when a redirect to the login page has no Location header.
    • A key store given as a KeyStore without a password, whose private key needs one, fails with a message that says how to set the password.
    • When SSL settings can’t be applied to an http request that is redirected to https, the SSLException ends with the message of the cause (for example a FileNotFoundException for a missing trust store) instead of “null”.
  • A request body sent with noContentType() no longer gets a Content-Type header. Before, a binary body (byte[], InputStream or File) got Content-Type: */*, and when an expected response content type was set (for example expect().contentType(ContentType.JSON)) that content type was sent as the request Content-Type. If the server needs a Content-Type, set it with contentType(..) instead of noContentType():

    given().contentType(ContentType.BINARY).body(bytes).when().post("/upload"); // instead of given().noContentType().body(bytes)
  • A request body that isn’t a String (for example a byte[], InputStream or File) sent with content-type application/x-www-form-urlencoded now fails with an IllegalArgumentException explaining that a form url-encoded body must be a String. Before, it failed with a groovy.lang.MissingMethodException. Send the body as a String, or use formParam(..).

  • A groovy.lang.Writable request body sent with a text content-type is now sent as its toString() instead of what its writeTo(..) method writes. Groovy’s own Writables (such as JsonBuilder and GString) produce the same output either way. A Groovy WritableFile (file.asWritable()) is now sent like any other File, read with the charset of the request content-type. If your own Writable writes something other than its toString(), write it to a String yourself and send that.

  • EncoderConfig.defaultCharsetForContentType(charset, contentType) now only sets the default charset of the given content type. It used to also change the default content charset (defaultContentCharset()), so the charset was used for every content type without a charset of its own. For example, after encoderConfig().defaultCharsetForContentType("UTF-16", ContentType.JSON) a plain text body was sent as text/plain; charset=UTF-16 and is now sent as text/plain; charset=ISO-8859-1. If you relied on this, also call defaultContentCharset(..):

    encoderConfig().defaultCharsetForContentType("UTF-16", ContentType.JSON).defaultContentCharset("UTF-16")

    The same change was made to DecoderConfig, see Responses.

These changes apply when REST Assured reads the login page to find the form. A form action given to FormAuthConfig is still used as it is.

  • Form authentication now fails with an IllegalArgumentException that names the field when an additional input field given to FormAuthConfig.withAdditionalField(s)(..) isn’t on the login page. Such a field used to be sent with the value []. Correct the field name, or remove it from withAdditionalField(s)(..).
  • A login form with an empty action (action="") is now posted to the URL of the login page, like a browser does, instead of to /.
  • A relative form action is now resolved against the URL of the login page (or its <base href>), like a browser does. An action without a leading slash used to be resolved against the root, so for example action="login" on the page /secured/page was posted to /login and is now posted to /secured/login.

If your server expects the old target in either case, give the form action in FormAuthConfig:

given().auth().form("John", "Doe", new FormAuthConfig("/login", "j_username", "j_password")). ..
  • auth().certificate(certURL, password) and RestAssured.certificate(certURL, password), with or without CertificateAuthSettings, now use the file at certURL as key store, as documented, so the client certificate in it is sent to servers that ask for one (mutual TLS). Before, the file was only used as trust store, which it still is. This means that:

    • A private key in the file is now offered to any server that asks for a client certificate, also where it’s optional (such as nginx with ssl_verify_client optional). Such a server may then reject a certificate it doesn’t trust.
    • The private key must have the password of the file.

    To avoid this, or to use the JVM’s default trust store, use the overload that takes a separate trust store and key store. An empty path means no key store, or the default trust store:

    // Use the file only as trust store, as before (no client certificate is sent)
    RestAssured.authentication = certificate("/truststore.jks", "password", "", "", certAuthSettings());

    certificate(..) with five arguments returns an AuthenticationScheme, so you can also pass it to RequestSpecBuilder.setAuth(..).

  • A key store configured without a trust store (with keyStore(..) on a request specification, RequestSpecBuilder, RestAssured or SSLConfig) is now used, so REST Assured offers its client certificate to a server that asks for one. Before, the key store was loaded but ignored unless a trust store was configured too, so a server that requires a client certificate rejected the connection. How the server certificate is validated is unchanged (the JVM’s default trust when no trust store is configured). Things to look out for:

    • A server that only requests a client certificate (for example nginx with ssl_verify_client optional) and doesn’t trust it now rejects a connection that used to work. If you don’t need a client certificate for that server, remove the keyStore(..) call.
    • A key store whose private key can’t be read with the key store password now fails the request instead of being ignored.
    • A key store given as a KeyStore without a password, whose private key needs one, fails with an IllegalArgumentException that says how to set the password, for example:
    given().config(RestAssured.config().sslConfig(sslConfig().keyStore(keyStore).keyStore("password"))). ..
  • Merging a request specification with spec(..), specification(..) or RequestSpecBuilder.addRequestSpecification(..) no longer resets values that the merged specification never set. The base URI, base path, port, authentication scheme, request body, proxy, HTTP method, path (together with its unnamed path parameters), whether url encoding is enabled, whether a Content-Type is allowed (noContentType()) and whether CSRF is disabled (disableCsrf()) now survive a merge, and are only overwritten when the merged specification sets them. Values that the merged specification only got from the defaults (such as RestAssured.baseURI, RestAssured.port, RestAssured.authentication or RestAssured.requestSpecification) don’t overwrite anything either. For example:

    given().auth().oauth2("token").spec(specWithOnlyAHeader). .. // now sends the Authorization header, before it sent none
    given().baseUri("http://host").port(8081).spec(specWithOnlyAHeader). .. // now uses http://host:8081, before it used RestAssured.baseURI and RestAssured.port

    This also affects a plain given(), which merges RestAssured.requestSpecification: it now uses the current values of RestAssured.baseURI, RestAssured.port, RestAssured.basePath and RestAssured.urlEncodingEnabled instead of the values they had when RestAssured.requestSpecification was built, unless that specification set them itself. To remove the authentication with a merged specification, set it to none explicitly:

    RequestSpecification noAuth = new RequestSpecBuilder().setAuth(new ExplicitNoAuthScheme()).build();
  • Sending a request directly from a request specification created by RequestSpecBuilder.build() (for example spec.when().get("/x") or spec.post()) now fails with an IllegalStateException that tells you to use given().spec(spec) or given(spec), instead of a NullPointerException. then(), expect() and response() on such a specification also throw this exception, where they returned null before. The specification isn’t changed by the failed call.

    given(spec).when().get("/x"); // instead of spec.when().get("/x")
  • given() no longer writes the session id into the RestAssured.config field, or RestAssured.defaultParser into the global parser registry. While RestAssured.sessionId is set, RestAssured.config() returns the config with the session id, but the RestAssured.config field doesn’t contain it, also not after a request. So given().config(RestAssured.config) no longer sends the static session id:

    given().config(RestAssured.config()). .. // instead of given().config(RestAssured.config)

    Setting RestAssured.sessionId or RestAssured.defaultParser back to null now takes effect without RestAssured.reset(). A list returned by RestAssured.filters() before replaceFiltersWith(..) keeps showing the old filters.

  • Sending a request directly from a response specification created by ResponseSpecBuilder.build() (for example responseSpec.when().get("/x")) now fails with an IllegalStateException instead of a NullPointerException. when(), given(), with() and request() on such a specification also throw this exception, where they returned null before. Using the specification in a request, or as RestAssured.responseSpecification, works as before.

    given(requestSpec, responseSpec).when().get("/x"); // or given().spec(requestSpec).expect().spec(responseSpec)
  • Using a RequestSpecification that wasn’t created by REST Assured with spec(..) or specification(..) on a request specification now fails with an IllegalArgumentException instead of a groovy.lang.MissingMethodException.

  • Using a ResponseSpecification that wasn’t created by REST Assured, with spec(..) or specification(..) on a response specification or as RestAssured.responseSpecification, now fails with an IllegalArgumentException instead of a groovy.lang.MissingMethodException.

Custom extensions written against internal or Groovy types

Section titled “Custom extensions written against internal or Groovy types”

These changes only affect you if you implement or call REST Assured’s SPI or io.restassured.internal classes, or rely on them being Groovy objects.

  • A Groovy closure used as request body now fails with an IllegalArgumentException. This could only happen when a custom ObjectMapper returned a Closure from serialize(..), in which case the closure was run as a builder (a JsonBuilder for JSON, a StreamingMarkupBuilder for XML and HTML, or with a Writer or OutputStream for text and binary content). Return a String, byte[] or InputStream from the ObjectMapper instead.
  • The authentication schemes in io.restassured.authentication (BasicAuthScheme, CertAuthScheme, ExplicitNoAuthScheme, FormAuthScheme, NTLMAuthScheme, NoAuthScheme, OAuth2Scheme, OAuthScheme, PreemptiveBasicAuthScheme and PreemptiveOAuth2HeaderScheme) and the AuthenticationScheme interface are now written in Java, and the classes no longer implement groovy.lang.GroovyObject. Their constructors, getters and setters are unchanged. A custom AuthenticationScheme written in Java can now get the authentication configuration from HTTPBuilder.getAuth().
  • io.restassured.internal.http.HTTPBuilder, which a custom AuthenticationScheme receives in authenticate(..), no longer uses Groovy closures. Its get(..) methods, the request(..) overloads that took a Closure, getHandler() and the post(..) and patch(..) overloads without a response handler are removed. post(..), patch(..) and request(..) now take an HttpResponseHandler or a RequestConfigurer, and response handlers always take two arguments (the response and its parsed content).
  • io.restassured.internal.RestAssuredResponseOptionsGroovyImpl is removed and its logic now lives in RestAssuredResponseOptionsImpl, so getGroovyResponse(), setGroovyResponse(..) and the protected groovyResponse field of RestAssuredResponseOptionsImpl are gone. This affects code that calls these methods or subclasses the internal response classes. A subclass that overrides a method such as asString() now also sees that override used by path(..), jsonPath(), as(..) and the other methods that read the body.
  • io.restassured.assertion.HeaderMatcher, DetailedCookieAssertion, ResponseTimeMatcher and StreamVerifier are now written in Java and no longer implement groovy.lang.GroovyObject. HeaderMatcher.validateHeader(..), DetailedCookieAssertion.validateCookies(..) and ResponseTimeMatcher.validate(..) now return Map<String, Object> instead of Object, HeaderMatcher.validateHeader(..) takes a Response, and HeaderMatcher.getMatcher() and get/setResponseAwareMatcher(..) are now generically typed (Matcher<?> and ResponseAwareMatcher<Response>).
  • io.restassured.internal.matcher.xml.XmlXsdMatcher (the return type of RestAssuredMatchers.matchesXsd(..)) and XmlDtdMatcher are now written in Java and no longer implement groovy.lang.GroovyObject.
  • path(..) on a response with an HTML content type (for example get("/page.html").path("html.head.title") or then().extract().path(..)) now returns the value the path points to, like it does for JSON and XML responses and like body("html.head.title", ..) does. Before, it returned the XmlPath itself, so extracting the value to a String failed with a ClassCastException. Use htmlPath() to get an XmlPath for an HTML response:

    String title = get("/page.html").path("html.head.title");
    XmlPath html = get("/page.html").htmlPath(); // instead of get("/page.html").path(..) to get the XmlPath
  • DecoderConfig.defaultCharsetForContentType(charset, contentType) now only sets the default charset of the given content type. It used to also change the default content charset (defaultContentCharset()), so a response without a charset in its content-type was decoded with that charset even when its content type had no charset of its own. For example, after decoderConfig().defaultCharsetForContentType("UTF-16", ContentType.JSON) a text/plain response without a charset was decoded as UTF-16 and is now decoded with the default content charset. If you relied on this, also call defaultContentCharset(..):

    decoderConfig().defaultCharsetForContentType("UTF-16", ContentType.JSON).defaultContentCharset("UTF-16")
  • matchesXsdInClasspath(..) and matchesDtdInClasspath(..) (in RestAssuredMatchers, RestAssuredMockMvcMatchers and RestAssuredWebTestClientMatchers) now fail with an IllegalArgumentException saying that the XSD or DTD couldn’t be found in classpath, instead of a groovy.lang.GroovyRuntimeException (“Ambiguous method overloading”). The XSD and DTD matchers now also fail with an IllegalArgumentException when asked to match something that isn’t a String, for example body("some.path", matchesXsd(..)) where the path returns an element that has child elements, instead of a Groovy exception.

  • Upgraded the dependencies of the published modules. scala-library in scala-support goes from 2.13.0 to 2.13.18, which fixes CVE-2022-36944. Also upgraded: scala3-library_3 in scala-extensions to 3.3.8, Jackson 2 to 2.22.3, Jackson 3 (tools.jackson) to 3.2.3, Gson to 2.14.0, Guava in json-schema-validator to 33.7.2-jre, commons-lang3 to 3.21.0, Yasson to 3.0.5 (now with its new group id org.eclipse.yasson), Parsson to 1.1.9, Johnzon to 2.2.0 and jaxb-impl to 2.3.9 (#1986).
  • Fixed spring-web-test-client-kotlin-extensions bringing jakarta.servlet-api, jakarta.jakartaee-web-api and about 20 other Jakarta EE API jars to your runtime classpath. They are now provided, like in spring-mock-mvc-kotlin-extensions (#1992).
  • Upgraded Groovy from 5.0.3 to 5.0.8 and Apache HttpClient (httpclient and httpmime) from 4.5.13 to 4.5.14 (#1971).
  • Fixed PATCH requests with form parameters or multipart content, which were sent without their query parameters (#1001).
  • Fixed sending a String body or form parameters with noContentType(), which failed unless an expected response content type was set. The body is now sent as text in the default content charset, so the encodeContentTypeAs("*/*", ContentType.TEXT) workaround is no longer needed (#1947).
  • Fixed a request hanging forever when its path contains a { that is never closed, for example get("/x{b"). An unclosed { is now kept as literal text (#1935).
  • Fixed a byte[] multipart being sent empty the second time a request specification is used (#1938).
  • Fixed a multipart boundary that contains =, for example a JavaMail-style boundary such as ----=_Part_0_123, being replaced or cut short. The boundary is now used as given (#1948).
  • Fixed multiPart(String, Object) with a String and no request content type. It now adds a text/plain part (#1930).
  • Fixed keyStore(KeyStore) on a request specification and RequestSpecBuilder.setKeyStore(KeyStore), which always failed. SSLConfig gets a keyStore(KeyStore) method. relaxedHTTPSValidation() only uses a key store given by path, not a KeyStore object (#1930).
  • Form authentication fixes:
    • Requests with path parameters, for example given().auth().form("John", "Doe").get("/secured/{id}", 1), no longer fail with “Invalid number of path parameters”, and the login page is no longer URL encoded twice (#1940).
    • A login form without an action attribute is posted to the URL of the login page instead of failing with a NullPointerException (#1941).
    • A SessionFilter subclass is passed on to the login request (#1941).
    • An additional field whose input tag has no value attribute is sent without a value instead of failing (#1921).
    • A form action read from the login page is no longer URL encoded twice, and an absolute form action’s path and query are posted to the host of the request (#1954).
  • Fixed CsrfConfig.csrfMetaTagName(..), which also set the CSRF input field name (#1945).
  • Fixed LogConfig.defaultStream(..), which always turned pretty printing back on (#1946).
  • Fixed merging a request specification that has unnamed path parameters, for example the one a filter receives for get("/users/{id}", 1) (#1931).
  • Fixed a root path with more than one placeholder left for a later withArgs(..), which failed with a MissingFormatArgumentException. body(withArgs(..), matcher) no longer adds a trailing dot to the path in failure messages (#1949).
  • Fixed FilterContext.send(..) for TRACE and custom HTTP methods such as request("PURGE", "/x") (#1929).
  • Fixed getResponseContentType() on the response specification that filters receive, which always failed with a StackOverflowError (#1928).
  • Fixed asByteArray() on a response whose body was replaced by an InputStream, for example by a filter (#1926).
  • Fixed sessionId() on Spring MockMvc, Spring WebTestClient and ResponseBuilder responses, and header(name) on a response without headers, which now returns null (#1944).
  • Fixed a ResponseAwareMatcher in a static RestAssuredMockMvc.responseSpecification or RestAssuredWebTestClient.responseSpecification failing when it calls path(..), and ignoring the configured session id name (#1944).
  • XSD and DTD matcher fixes:
    • matchesDtd(URL) always failed with a GroovyCastException (#1920).
    • The same matcher can now be used more than once, for example in a shared ResponseSpecification. An InputStream or Reader given to these matchers is now read and closed when the matcher is created (#1936).
    • matchesXsdInClasspath(..) and matchesDtdInClasspath(..) also look in the class loader that loaded REST Assured, so they work on threads without a usable context class loader (#1937).
  • Fixed JsonPath and JSON body expectations failing with ClassNotFoundException: groovy.lang.Script in an OSGi container, or whenever the thread context class loader can’t see Groovy (#1933).
  • Non-String names and values that used to fail with a Groovy exception are now converted to their string form, so 1 becomes "1": parameter names in params(Map), queryParams(Map), formParams(Map) and pathParams(Map) (#1927), cookie values in cookies(..) (#1930), header and cookie names in headers(..) and cookies(..) (#1931), header names in header expectations (#1919) and body expectation paths (#1928).
  • A list given as path, or as path arguments to body(..), rootPath(..), root(..) or appendRootPath(..), whose elements aren’t all created by withArgs(..), now fails with an IllegalArgumentException (#1928). A CertAuthScheme key store or trust store path that is neither a String nor a File also fails with an IllegalArgumentException (#1925).

See change log for more details.