Vert.x Web Client

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>3.9.16</version>
</dependency>
  • Gradle (in your build.gradle file):

dependencies {
 compile 'io.vertx:vertx-web-client:3.9.16'
}

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

WebClient client = WebClient.create(vertx);

If you want to configure options for the client, you create it as follows

WebClientOptions options = new WebClientOptions()
  .setUserAgent("My-App/1.2.3");
options.setKeepAlive(false);
WebClient 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

WebClient 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

WebClient client = WebClient.create(vertx);

// Send a GET request
client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .send(ar -> {
    if (ar.succeeded()) {
      // Obtain response
      HttpResponse<Buffer> response = ar.result();

      System.out.println("Received response with status code" + response.statusCode());
    } else {
      System.out.println("Something went wrong " + ar.cause().getMessage());
    }
  });

// Send a HEAD request
client
  .head(8080, "myserver.mycompany.com", "/some-uri")
  .send(ar -> {
    if (ar.succeeded()) {
      // Obtain response
      HttpResponse<Buffer> response = ar.result();

      System.out.println("Received response with status code" + response.statusCode());
    } else {
      System.out.println("Something went wrong " + ar.cause().getMessage());
    }
  });

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")
  .send(ar -> {
    if (ar.succeeded()) {
      // Obtain response
      HttpResponse<Buffer> response = ar.result();

      System.out.println("Received response with status code" + response.statusCode());
    } else {
      System.out.println("Something went wrong " + ar.cause().getMessage());
    }
  });

Any request URI parameter will pre-populate the request

HttpRequest<Buffer> request = client.get(8080, "myserver.mycompany.com", "/some-uri?param1=param1_value&param2=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

HttpRequest<Buffer> 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&param2=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

client
  .post(8080, "myserver.mycompany.com", "/some-uri")
  .sendBuffer(buffer, ar -> {
    if (ar.succeeded()) {
      // Ok
    }
  });

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

client
  .post(8080, "myserver.mycompany.com", "/some-uri")
  .sendStream(stream, ar -> {
    if (ar.succeeded()) {
      // Ok
    }
  });

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.open("content.txt", new OpenOptions(), fileRes -> {
  if (fileRes.succeeded()) {
    ReadStream<Buffer> fileStream = fileRes.result();

    String fileLen = "1024";

    // Send the file to the server using POST
    client
      .post(8080, "myserver.mycompany.com", "/some-uri")
      .putHeader("content-length", fileLen)
      .sendStream(fileStream, ar -> {
        if (ar.succeeded()) {
          // Ok
        }
      });
  }
});

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")
  .sendJsonObject(new JsonObject()
    .put("firstName", "Dale")
    .put("lastName", "Cooper"), ar -> {
    if (ar.succeeded()) {
      // Ok
    }
  });

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")
  .sendJson(new User("Dale", "Cooper"), ar -> {
    if (ar.succeeded()) {
      // Ok
    }
  });
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.

MultiMap 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")
  .sendForm(form, ar -> {
    if (ar.succeeded()) {
      // Ok
    }
  });

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

MultiMap 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")
  .sendForm(form, ar -> {
    if (ar.succeeded()) {
      // Ok
    }
  });

If you want to upload files and send attributes, you can create a MultipartForm and use sendMultipartForm.

MultipartForm 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")
  .sendMultipartForm(form, ar -> {
    if (ar.succeeded()) {
      // Ok
    }
  });

Writing request headers

You can write headers to a request using the headers multi-map as follows:

HttpRequest<Buffer> request = client.get(8080, "myserver.mycompany.com", "/some-uri");
MultiMap 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

HttpRequest<Buffer> 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:

HttpRequest<Buffer> 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:

HttpRequest<Buffer> 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

HttpRequest<Buffer> get = client.get(8080, "myserver.mycompany.com", "/some-uri");
get.send(ar -> {
  if (ar.succeeded()) {
    // Ok
  }
});

// Same request again
get.send(ar -> {
  if (ar.succeeded()) {
    // Ok
  }
});

Beware though that HttpRequest instances are mutable. Therefore you should call the copy method before modifying a cached instance.

HttpRequest<Buffer> get = client.get(8080, "myserver.mycompany.com", "/some-uri");
get.send(ar -> {
  if (ar.succeeded()) {
    // Ok
  }
});

// The "get" request instance remains unmodified
get.copy().putHeader("a-header", "with-some-value").send(ar -> {
  if (ar.succeeded()) {
    // Ok
  }
});

Timeouts

You can set a timeout for a specific http request using timeout.

client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .timeout(5000)
  .send(ar -> {
    if (ar.succeeded()) {
      // Ok
    } else {
      // Might be a timeout when cause is java.util.concurrent.TimeoutException
    }
  });

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")
  .send(ar -> {
    if (ar.succeeded()) {

      HttpResponse<Buffer> response = ar.result();

      System.out.println("Received response with status code" + response.statusCode());
    } else {
      System.out.println("Something went wrong " + ar.cause().getMessage());
    }
  });
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 404 Not Found response, or a response with the wrong content type, are not considered as failures. Use response predicates if you want the Web Client to perform sanity checks automatically.

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

  • WriteStream

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())
  .send(ar -> {
    if (ar.succeeded()) {
      HttpResponse<JsonObject> response = ar.result();

      JsonObject body = response.body();

      System.out.println("Received response with status code" + response.statusCode() + " with body " + body);
    } else {
      System.out.println("Something went wrong " + ar.cause().getMessage());
    }
  });

In Java, Groovy or Kotlin, custom Json mapped POJO can be decoded

client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .as(BodyCodec.json(User.class))
  .send(ar -> {
    if (ar.succeeded()) {
      HttpResponse<User> response = ar.result();

      User user = response.body();

      System.out.println("Received response with status code" + response.statusCode() + " with body " +
        user.getFirstName() + " " + user.getLastName());
    } else {
      System.out.println("Something went wrong " + ar.cause().getMessage());
    }
  });

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))
  .send(ar -> {
    if (ar.succeeded()) {

      HttpResponse<Void> response = ar.result();

      System.out.println("Received response with status code" + response.statusCode());
    } else {
      System.out.println("Something went wrong " + ar.cause().getMessage());
    }
  });

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())
  .send(ar -> {
    if (ar.succeeded()) {

      HttpResponse<Void> response = ar.result();

      System.out.println("Received response with status code" + response.statusCode());
    } else {
      System.out.println("Something went wrong " + ar.cause().getMessage());
    }
  });

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")
  .send(ar -> {
    if (ar.succeeded()) {

      HttpResponse<Buffer> response = ar.result();

      // Decode the body as a json object
      JsonObject body = response.bodyAsJsonObject();

      System.out.println("Received response with status code" + response.statusCode() + " with body " + body);
    } else {
      System.out.println("Something went wrong " + ar.cause().getMessage());
    }
  });
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")
  .send(ar -> {
    if (ar.succeeded()) {

      HttpResponse<Buffer> response = ar.result();

      if (response.statusCode() == 200 && response.getHeader("content-type").equals("application/json")) {

        // Decode the body as a json object
        JsonObject body = response.bodyAsJsonObject();
        System.out.println("Received response with status code" + response.statusCode() + " with body " + body);

      } else {
        System.out.println("Something went wrong " + response.statusCode());
      }

    } else {
      System.out.println("Something went wrong " + ar.cause().getMessage());
    }
  });

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)
  .send(ar -> {
    if (ar.succeeded()) {

      HttpResponse<Buffer> response = ar.result();

      // Safely decode the body as a json object
      JsonObject body = response.bodyAsJsonObject();
      System.out.println("Received response with status code" + response.statusCode() + " with body " + body);

    } else {
      System.out.println("Something went wrong " + ar.cause().getMessage());
    }
  });

You can also create custom predicates when existing predicates don’t fit your needs:

Function<HttpResponse<Void>, ResponsePredicateResult> methodsPredicate = resp -> {
  String methods = resp.getHeader("Access-Control-Allow-Methods");
  if (methods != null) {
    if (methods.contains("POST")) {
      return ResponsePredicateResult.success();
    }
  }
  return 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)
  .send(ar -> {
    if (ar.succeeded()) {
      // Process the POST request now
    } else {
      System.out.println("Something went wrong " + ar.cause().getMessage());
    }
  });
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))
  .send(ar -> {
    // ....
  });

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"))
  .send(ar -> {
    // ....
  });

Please refer to the ResponsePredicate documentation for a full list of predefined predicates.

Creating custom failures

By default, response predicates (including the predefined ones) use a default error converter which discards the body and conveys a simple message. You can customize the exception class by changing the error converter:

ResponsePredicate predicate = ResponsePredicate.create(ResponsePredicate.SC_SUCCESS, result -> {
  return new MyCustomException(result.message());
});

Many web APIs provide details in error responses. For example, the Marvel API uses this JSON object format:

{
 "code": "InvalidCredentials",
 "message": "The passed API key is invalid."
}

To avoid losing this information, it is possible to wait for the response body to be fully received before the error converter is called:

ErrorConverter converter = ErrorConverter.createFullBody(result -> {

  // Invoked after the response body is fully received
  HttpResponse<Buffer> response = result.response();

  if (response.getHeader("content-type").equals("application/json")) {
    // Error body is JSON data
    JsonObject body = response.bodyAsJsonObject();
    return new MyCustomException(body.getString("code"), body.getString("message"));
  }

  // Fallback to defaut message
  return new MyCustomException(result.message());
});

ResponsePredicate predicate = ResponsePredicate.create(ResponsePredicate.SC_SUCCESS, converter);
Warning
creating exception in Java can have a performance cost when it captures a stack trace, so you might want to create exceptions that do not capture the stack trace. By default exceptions are reported using the an exception that does not capture the stack trace.

Handling 30x redirections

By default the client follows redirections, you can configure the default behavior in the WebClientOptions:

WebClient client = WebClient.create(vertx, new WebClientOptions().setFollowRedirects(false));

The client will follow at most 16 requests redirections, it can be changed in the same options:

WebClient client = WebClient.create(vertx, new 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)
  .send(ar -> {
    if (ar.succeeded()) {
      // Obtain response
      HttpResponse<Buffer> response = ar.result();

      System.out.println("Received response with status code" + response.statusCode());
    } else {
      System.out.println("Something went wrong " + ar.cause().getMessage());
    }
  });

Or using create methods with absolute URI argument

client
  .getAbs("https://myserver.mycompany.com:4043/some-uri")
  .send(ar -> {
    if (ar.succeeded()) {
      // Obtain response
      HttpResponse<Buffer> response = ar.result();

      System.out.println("Received response with status code" + response.statusCode());
    } else {
      System.out.println("Something went wrong " + ar.cause().getMessage());
    }
  });

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

WebClient client = WebClient.create(vertx);
WebClientSession session = 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:

WebClientSession session = 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.

RxJava 2 API

The RxJava HttpRequest provides an rx-ified version of the original API, the rxSend method returns a Single<HttpResponse<Buffer>> that makes the HTTP request upon subscription, as consequence, the Single can be subscribed many times.

Single<HttpResponse<Buffer>> single = client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .rxSend();

// Send a request upon subscription of the Single
single.subscribe(response -> System.out.println("Received 1st response with status code" + response.statusCode()), error -> System.out.println("Something went wrong " + error.getMessage()));

// Send another request
single.subscribe(response -> System.out.println("Received 2nd response with status code" + response.statusCode()), error -> System.out.println("Something went wrong " + error.getMessage()));

The obtained Single can be composed and chained naturally with the RxJava API

Single<String> url = client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .rxSend()
  .map(HttpResponse::bodyAsString);

// Use the flatMap operator to make a request on the URL Single
url
  .flatMap(u -> client.getAbs(u).rxSend())
  .subscribe(response -> System.out.println("Received response with status code" + response.statusCode()), error -> System.out.println("Something went wrong " + error.getMessage()));

The same APIs is available

Single<HttpResponse<JsonObject>> single = client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .putHeader("some-header", "header-value")
  .addQueryParam("some-param", "param value")
  .as(BodyCodec.jsonObject())
  .rxSend();
single.subscribe(resp -> {
  System.out.println(resp.statusCode());
  System.out.println(resp.body());
});

The sendStream shall be preferred for sending bodies Flowable<Buffer>.

Flowable<Buffer> body = getPayload();

Single<HttpResponse<Buffer>> single = client
  .post(8080, "myserver.mycompany.com", "/some-uri")
  .rxSendStream(body);
single.subscribe(resp -> {
  System.out.println(resp.statusCode());
  System.out.println(resp.body());
});

Upon subscription, the body will be subscribed and its content used for the request.

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.

SocketAddress 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())
  .send(ar -> {
    if (ar.succeeded()) {
      // Obtain response
      HttpResponse<JsonObject> response = ar.result();

      System.out.println("Current Docker images" + response.body());
    } else {
      System.out.println("Something went wrong " + ar.cause().getMessage());
    }
  });