Vert.x Web Client is an asynchronous HTTP and HTTP/2 client.
The Web Client makes easy to do HTTP request/response interactions with a web server, and provides advanced features like:
-
Json body encoding / decoding
-
request/response pumping
-
request parameters
-
unified error handling
-
form submissions
The Web Client does not deprecate the Vert.x Core HttpClient
, indeed it is based on
this client and inherits its configuration and great features like pooling, HTTP/2 support, pipelining support, etc…
The HttpClient
should be used when fine grained control over the HTTP
requests/responses is necessary.
The Web Client does not provide a WebSocket API, the Vert.x Core HttpClient
should
be used. It also does not handle cookies at the moment.
Using the Web Client
To use Vert.x Web Client, add the following dependency to the dependencies section of your build descriptor:
-
Maven (in your
pom.xml
):
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web-client</artifactId>
<version>${maven.version}</version>
</dependency>
-
Gradle (in your
build.gradle
file):
dependencies {
compile 'io.vertx:vertx-web-client:${maven.version}'
}
Re-cap on Vert.x core HTTP client
Vert.x Web Client uses the API from Vert.x core, so it’s well worth getting familiar with the basic concepts of using
HttpClient
using Vert.x core, if you’re not already.
Creating a Web Client
You create an WebClient
instance with default options as follows
var client = WebClient.create(vertx)
If you want to configure options for the client, you create it as follows
var options = WebClientOptions()
.setUserAgent("My-App/1.2.3")
options.setKeepAlive(false)
var client = WebClient.create(vertx, options)
Web Client options inherit Http Client options so you can set any one of them.
If your already have an HTTP Client in your application you can also reuse it
var client = WebClient.wrap(httpClient)
Important
|
In most cases, a Web Client should be created once on application startup and then reused. Otherwise you lose a lot of benefits such as connection pooling and may leak resources if instances are not closed properly. |
Making requests
Simple requests with no body
Often, you’ll want to make HTTP requests with no request body. This is usually the case with HTTP GET, OPTIONS and HEAD requests
var client = WebClient.create(vertx)
// Send a GET request
client.get(8080, "myserver.mycompany.com", "/some-uri").sendFuture().onComplete{
case Success(result) => {
// Obtain response
var response = result
println(s"Received response with status code${response.statusCode()}")
}
case Failure(cause) => {
println(s"$cause")
}
}
// Send a HEAD request
client.head(8080, "myserver.mycompany.com", "/some-uri").sendFuture().onComplete{
case Success(result) => {
// Obtain response
var response = result
println(s"Received response with status code${response.statusCode()}")
}
case Failure(cause) => {
println(s"$cause")
}
}
You can add query parameters to the request URI in a fluent fashion
client.get(8080, "myserver.mycompany.com", "/some-uri").addQueryParam("param", "param_value").sendFuture().onComplete{
case Success(result) => {
// Obtain response
var response = result
println(s"Received response with status code${response.statusCode()}")
}
case Failure(cause) => {
println(s"$cause")
}
}
Any request URI parameter will pre-populate the request
var request = client.get(8080, "myserver.mycompany.com", "/some-uri?param1=param1_value¶m2=param2_value")
// Add param3
request.addQueryParam("param3", "param3_value")
// Overwrite param2
request.setQueryParam("param2", "another_param2_value")
Setting a request URI discards existing query parameters
var request = client.get(8080, "myserver.mycompany.com", "/some-uri")
// Add param1
request.addQueryParam("param1", "param1_value")
// Overwrite param1 and add param2
request.uri("/some-uri?param1=param1_value¶m2=param2_value")
Writing request bodies
When you need to make a request with a body, you use the same API and call then sendXXX
methods
that expects a body to send.
Use sendBuffer
to send a buffer body
// Send a buffer to the server using POST, the content-length header will be set for you
client.post(8080, "myserver.mycompany.com", "/some-uri").sendBufferFuture(buffer).onComplete{
case Success(result) => {
// Ok
}
case Failure(cause) => println("Failure")
}
Sending a single buffer is useful but often you don’t want to load fully the content in memory because
it may be too large or you want to handle many concurrent requests and want to use just the minimum
for each request. For this purpose the Web Client can send ReadStream<Buffer>
(e.g a
AsyncFile
is a ReadStream<Buffer>`) with the sendStream
method
// When the stream len is unknown sendStream sends the file to the server using chunked transfer encoding
client.post(8080, "myserver.mycompany.com", "/some-uri").sendStreamFuture(stream).onComplete{
case Success(result) => {
// Ok
}
case Failure(cause) => println("Failure")
}
The Web Client takes care of setting up the transfer pump for you. Since the length of the stream is not know the request will use chunked transfer encoding .
When you know the size of the stream, you shall specify before using the content-length
header
fs.openFuture("content.txt", OpenOptions()).onComplete{
case Success(result) => {
var fileStream = result
var fileLen = "1024"
// Send the file to the server using POST
client.post(8080, "myserver.mycompany.com", "/some-uri").putHeader("content-length", fileLen).sendStreamFuture(fileStream).onComplete{
case Success(result) => {
// Ok
}
case Failure(cause) => println("Failure")
}
}
case Failure(cause) => println("Failure")
}
The POST will not be chunked.
Json bodies
Often you’ll want to send Json body requests, to send a JsonObject
use the sendJsonObject
client.post(8080, "myserver.mycompany.com", "/some-uri").sendJsonObjectFuture(new io.vertx.core.json.JsonObject().put("firstName", "Dale").put("lastName", "Cooper")).onComplete{
case Success(result) => {
// Ok
}
case Failure(cause) => println("Failure")
}
In Java, Groovy or Kotlin, you can use the sendJson
method that maps
a POJO (Plain Old Java Object) to a Json object using Json.encode
method
client.post(8080, "myserver.mycompany.com", "/some-uri").sendJsonFuture(new examples.WebClientExamples.User("Dale", "Cooper")).onComplete{
case Success(result) => {
// Ok
}
case Failure(cause) => println("Failure")
}
Note
|
the Json.encode uses the Jackson mapper to encode the object
to Json.
|
Form submissions
You can send http form submissions bodies with the sendForm
variant.
var form = MultiMap.caseInsensitiveMultiMap()
form.set("firstName", "Dale")
form.set("lastName", "Cooper")
// Submit the form as a form URL encoded body
client.post(8080, "myserver.mycompany.com", "/some-uri").sendFormFuture(form).onComplete{
case Success(result) => {
// Ok
}
case Failure(cause) => println("Failure")
}
By default the form is submitted with the application/x-www-form-urlencoded
content type header. You can set
the content-type
header to multipart/form-data
instead
var form = MultiMap.caseInsensitiveMultiMap()
form.set("firstName", "Dale")
form.set("lastName", "Cooper")
// Submit the form as a multipart form body
client.post(8080, "myserver.mycompany.com", "/some-uri").putHeader("content-type", "multipart/form-data").sendFormFuture(form).onComplete{
case Success(result) => {
// Ok
}
case Failure(cause) => println("Failure")
}
If you want to upload files and send attributes, you can create a MultipartForm
and
use sendMultipartForm
.
var form = MultipartForm.create().attribute("imageDescription", "a very nice image").binaryFileUpload("imageFile", "image.jpg", "/path/to/image", "image/jpeg")
// Submit the form as a multipart form body
client.post(8080, "myserver.mycompany.com", "/some-uri").sendMultipartFormFuture(form).onComplete{
case Success(result) => {
// Ok
}
case Failure(cause) => println("Failure")
}
Writing request headers
You can write headers to a request using the headers multi-map as follows:
var request = client.get(8080, "myserver.mycompany.com", "/some-uri")
var headers = request.headers()
headers.set("content-type", "application/json")
headers.set("other-header", "foo")
The headers are an instance of MultiMap
which provides operations for adding,
setting and removing entries. Http headers allow more than one value for a specific key.
You can also write headers using putHeader
var request = client.get(8080, "myserver.mycompany.com", "/some-uri")
request.putHeader("content-type", "application/json")
request.putHeader("other-header", "foo")
Configure the request to add authentication.
Authentication can be performed manually by setting the correct headers, or, using our predefined methods (We strongly suggest having HTTPS enabled, especially for authenticated requests):
In basic HTTP authentication, a request contains a header field of the form Authorization: Basic <credentials>
,
where credentials is the base64 encoding of id and password joined by a colon.
You can configure the request to add basic access authentication as follows:
var request = client.get(8080, "myserver.mycompany.com", "/some-uri").basicAuthentication("myid", "mypassword")
In OAuth 2.0, a request contains a header field of the form Authorization: Bearer <bearerToken>
,
where bearerToken is the bearer token issued by an authorization server to access protected resources.
You can configure the request to add bearer token authentication as follows:
var request = client.get(8080, "myserver.mycompany.com", "/some-uri").bearerTokenAuthentication("myBearerToken")
Reusing requests
The send
method can be called multiple times
safely, making it very easy to configure and reuse HttpRequest
objects
var get = client.get(8080, "myserver.mycompany.com", "/some-uri")
get.sendFuture().onComplete{
case Success(result) => {
// Ok
}
case Failure(cause) => println("Failure")
}
// Same request again
get.sendFuture().onComplete{
case Success(result) => {
// Ok
}
case Failure(cause) => println("Failure")
}
Beware though that HttpRequest
instances are mutable.
Therefore you should call the copy
method before modifying a cached instance.
var get = client.get(8080, "myserver.mycompany.com", "/some-uri")
get.sendFuture().onComplete{
case Success(result) => {
// Ok
}
case Failure(cause) => println("Failure")
}
// The "get" request instance remains unmodified
get.copy().putHeader("a-header", "with-some-value").sendFuture().onComplete{
case Success(result) => {
// Ok
}
case Failure(cause) => println("Failure")
}
Timeouts
You can set a timeout for a specific http request using timeout
.
client.get(8080, "myserver.mycompany.com", "/some-uri").timeout(5000).sendFuture().onComplete{
case Success(result) => {
// Ok
}
case Failure(cause) => {
println(s"$cause")
}
}
If the request does not return any data within the timeout period an exception will be passed to the response handler.
Handling http responses
When the Web Client sends a request you always deal with a single async result HttpResponse
.
On a success result the callback happens after the response has been received
client.get(8080, "myserver.mycompany.com", "/some-uri").sendFuture().onComplete{
case Success(result) => {
var response = result
println(s"Received response with status code${response.statusCode()}")
}
case Failure(cause) => {
println(s"$cause")
}
}
Caution
|
By default, a Vert.x Web Client request ends with an error only if something wrong happens at the network level.
In other words, a |
Warning
|
Responses are fully buffered, use BodyCodec.pipe
to pipe the response to a write stream
|
Decoding responses
By default the Web Client provides an http response body as a Buffer
and does not apply
any decoding.
Custom response body decoding can be achieved using BodyCodec
:
-
Plain String
-
Json object
-
Json mapped POJO
A body codec can decode an arbitrary binary data stream into a specific object instance, saving you the decoding step in your response handlers.
Use BodyCodec.jsonObject
To decode a Json object:
client.get(8080, "myserver.mycompany.com", "/some-uri").as(BodyCodec.jsonObject()).sendFuture().onComplete{
case Success(result) => {
var response = result
var body = response.body()
println(s"Received response with status code${response.statusCode()} with body ${body}")
}
case Failure(cause) => {
println(s"$cause")
}
}
In Java, Groovy or Kotlin, custom Json mapped POJO can be decoded
client.get(8080, "myserver.mycompany.com", "/some-uri").as(BodyCodec.json(examples.WebClientExamples.User.class)).sendFuture().onComplete{
case Success(result) => {
var response = result
var user = response.body()
println(s"Received response with status code${response.statusCode()} with body ${user.getFirstName()} ${user.getLastName()}")
}
case Failure(cause) => {
println(s"$cause")
}
}
When large response are expected, use the BodyCodec.pipe
.
This body codec pumps the response body buffers to a WriteStream
and signals the success or the failure of the operation in the async result response
client.get(8080, "myserver.mycompany.com", "/some-uri").as(BodyCodec.pipe(writeStream)).sendFuture().onComplete{
case Success(result) => {
var response = result
println(s"Received response with status code${response.statusCode()}")
}
case Failure(cause) => {
println(s"$cause")
}
}
Finally if you are not interested at all by the response content, the BodyCodec.none
simply discards the entire response body
client.get(8080, "myserver.mycompany.com", "/some-uri").as(BodyCodec.none()).sendFuture().onComplete{
case Success(result) => {
var response = result
println(s"Received response with status code${response.statusCode()}")
}
case Failure(cause) => {
println(s"$cause")
}
}
When you don’t know in advance the content type of the http response, you can still use the bodyAsXXX()
methods
that decode the response to a specific type
client.get(8080, "myserver.mycompany.com", "/some-uri").sendFuture().onComplete{
case Success(result) => {
var response = result
// Decode the body as a json object
var body = response.bodyAsJsonObject()
println(s"Received response with status code${response.statusCode()} with body ${body}")
}
case Failure(cause) => {
println(s"$cause")
}
}
Warning
|
this is only valid for the response decoded as a buffer. |
Response predicates
By default, a Vert.x Web Client request ends with an error only if something wrong happens at the network level.
In other words, you must perform sanity checks manually after the response is received:
client.get(8080, "myserver.mycompany.com", "/some-uri").sendFuture().onComplete{
case Success(result) => {
var response = result
if (response.statusCode() == 200 && response.getHeader("content-type") == "application/json") {
// Decode the body as a json object
var body = response.bodyAsJsonObject()
println(s"Received response with status code${response.statusCode()} with body ${body}")
} else {
println(s"Something went wrong ${response.statusCode()}")
}
}
case Failure(cause) => {
println(s"$cause")
}
}
You can trade flexibility for clarity and conciseness using response predicates.
Response predicates
can fail a request when the response does
not match a criteria.
The Web Client comes with a set of out of the box predicates ready to use:
client.get(8080, "myserver.mycompany.com", "/some-uri").expect(ResponsePredicate.SC_SUCCESS).expect(ResponsePredicate.JSON).sendFuture().onComplete{
case Success(result) => {
var response = result
// Safely decode the body as a json object
var body = response.bodyAsJsonObject()
println(s"Received response with status code${response.statusCode()} with body ${body}")
}
case Failure(cause) => {
println(s"$cause")
}
}
You can also create custom predicates when existing predicates don’t fit your needs:
// Check CORS header allowing to do POST
var methodsPredicate = (resp: io.vertx.scala.ext.web.client.HttpResponse<java.lang.Void>) => {
var methods = resp.getHeader("Access-Control-Allow-Methods")
if (methods != null) {
if (methods.contains("POST")) {
ResponsePredicateResult.success()
}
}
ResponsePredicateResult.failure("Does not work")
}
// Send pre-flight CORS request
client.request(HttpMethod.OPTIONS, 8080, "myserver.mycompany.com", "/some-uri").putHeader("Origin", "Server-b.com").putHeader("Access-Control-Request-Method", "POST").expect(methodsPredicate).sendFuture().onComplete{
case Success(result) => {
// Process the POST request now
}
case Failure(cause) => {
println(s"$cause")
}
}
Tip
|
Response predicates are evaluated before the response body is received. Therefore you can’t inspect the response body in a predicate test function. |
Predefined predicates
As a convenience, the Web Client ships a few predicates for common uses cases .
For status codes, e.g ResponsePredicate.SC_SUCCESS
to verify that the
response has a 2xx
code, you can also create a custom one:
client.get(8080, "myserver.mycompany.com", "/some-uri").expect(ResponsePredicate.status(200, 202)).sendFuture().onComplete{
case Success(result) => println("Success")
case Failure(cause) => println("Failure")
}
For content types, e.g ResponsePredicate.JSON
to verify that the
response body contains JSON data, you can also create a custom one:
client.get(8080, "myserver.mycompany.com", "/some-uri").expect(ResponsePredicate.contentType("some/content-type")).sendFuture().onComplete{
case Success(result) => println("Success")
case Failure(cause) => println("Failure")
}
Please refer to the ResponsePredicate
documentation for a full list of predefined predicates.
Handling 30x redirections
By default the client follows redirections, you can configure the default behavior in the WebClientOptions
:
// Change the default behavior to not follow redirects
var client = WebClient.create(vertx, WebClientOptions()
.setFollowRedirects(false)
)
The client will follow at most 16
requests redirections, it can be changed in the same options:
// Follow at most 5 redirections
var client = WebClient.create(vertx, WebClientOptions()
.setMaxRedirects(5)
)
Note
|
For security reason, client won’t follow redirects for request with methods different from GET or HEAD |
Using HTTPS
Vert.x Web Client can be configured to use HTTPS in exactly the same way as the Vert.x HttpClient
.
You can specify the behavior per request
client.get(443, "myserver.mycompany.com", "/some-uri").ssl(true).sendFuture().onComplete{
case Success(result) => {
// Obtain response
var response = result
println(s"Received response with status code${response.statusCode()}")
}
case Failure(cause) => {
println(s"$cause")
}
}
Or using create methods with absolute URI argument
client.getAbs("https://myserver.mycompany.com:4043/some-uri").sendFuture().onComplete{
case Success(result) => {
// Obtain response
var response = result
println(s"Received response with status code${response.statusCode()}")
}
case Failure(cause) => {
println(s"$cause")
}
}
Sessions management
Vert.x web offers a web session management facility; to use it, you create a
WebClientSession
for every user (session) and use it instead of the
WebClient
.
Creating a WebSession
You create a WebClientSession
instance as follows
var client = WebClient.create(vertx)
var session = io.vertx.ext.web.client.WebClientSession.create(client)
Making requests
Once created, a WebClientSession
can be used instead of a
WebClient
to do HTTP(s) requests and automatically manage any cookies received from the server(s)
you are calling.
Setting session level headers
You can set any session level headers to be added to every request as follows:
var session = io.vertx.ext.web.client.WebClientSession.create(client)
session.addHeader("my-jwt-token", jwtToken)
The headers will then be added to every request; notice that these headers will be sent to all hosts; if you need
to send different headers to different hosts, you have to add them manually to every single request and not to the
WebClientSession
.
Domain sockets
Since 3.7.1 the Web Client supports domain sockets, e.g you can interact with the local Docker daemon.
To achieve this, the Vertx
instance must be created using a native transport, you can read
the Vert.x core documentation that explains it clearly.
// Creates the unix domain socket address to access the Docker API
var serverAddress = SocketAddress.domainSocketAddress("/var/run/docker.sock")
// We still need to specify host and port so the request HTTP header will be localhost:8080
// otherwise it will be a malformed HTTP request
// the actual value does not matter much for this example
client.request(HttpMethod.GET, serverAddress, 8080, "localhost", "/images/json").expect(ResponsePredicate.SC_ACCEPTED).as(BodyCodec.jsonObject()).sendFuture().onComplete{
case Success(result) => {
// Obtain response
var response = result
println(s"Current Docker images${response.body()}")
}
case Failure(cause) => {
println(s"$cause")
}
}