HTTP response validation with the Vert.x Web Client

By de­fault, a Vert.x Web Client re­quest ends with an error only if some­thing wrong hap­pens at the net­work level. In other words, a 404 Not Found re­sponse, or a re­sponse with the wrong con­tent type, are not con­sid­ered as fail­ures.

Hence, you would usu­ally per­form san­ity checks man­u­ally after the re­sponse is re­ceived:

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();
      } else {
        System.out.println("Something went wrong " + response.statusCode());
      }
    } else {
      System.out.println("Something went wrong " + ar.cause().getMessage());
    }
  });

Start­ing with Vert.x 3.6, you can can trade flex­i­bil­ity for clar­ity and con­cise­ness using re­sponse pred­i­cates.

Response predicates

Re­sponse pred­i­cates can fail a re­quest when the re­sponse does not match cri­te­rion.

The Web Client mod­ule comes with a set of ready-​to-use pred­i­cates:

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();
    } else {
      System.out.println("Something went wrong " + ar.cause().getMessage());
    }
  });

The web is full of HTTP/JSON end­points, so there is no doubt the ResponsePredicate.SC_SUCCESS and ResponsePredicate.JSON can be handy.

Nev­er­the­less, you might also need to check that the sta­tus code is whithin a spe­cific range:

client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .expect(ResponsePredicate.status(200, 202))
  .send(ar -> {
    // ....
  });

Or that the con­tent is of a spe­cific type:

client
  .get(8080, "myserver.mycompany.com", "/some-uri")
  .expect(ResponsePredicate.contentType("some/content-type"))
  .send(ar -> {
    // ....
  });

Please refer to the ResponsePredicate doc­u­men­ta­tion for a full list of pre­de­fined pred­i­cates.

Custom predicates

Even­tu­ally, pred­i­cates were not de­signed for sta­tus code and con­tent type check­ing only, so feel free to cre­ate your own val­i­da­tion code:

// Check CORS header allowing to do POST
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());
    }
  });

Note that re­sponse pred­i­cates are eval­u­ated be­fore the re­sponse body is re­ceived. There­fore you can’t in­spect the re­sponse body in a pred­i­cate test func­tion, only sta­tus code, sta­tus mes­sage and re­sponse head­ers.

Dealing with failures

By de­fault, re­sponse pred­i­cates (in­clud­ing the pre­de­fined ones) use a generic error con­verter which dis­cards the re­sponse body and con­veys a sim­ple mes­sage. You can cus­tomize the ex­cep­tion class by chang­ing the error con­verter:

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

Be­ware that cre­at­ing ex­cep­tions in Java comes with the per­for­mance cost of cap­tur­ing the call stack. The generic error con­verter gen­er­ates ex­cep­tions that do not cap­ture it.

Reading details in error responses

Many web APIs pro­vide de­tails in error re­sponses. For ex­am­ple, the Mar­vel API uses this JSON ob­ject for­mat:

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

To avoid los­ing this in­for­ma­tion, it is pos­si­ble to wait for the re­sponse body to be fully re­ceived be­fore the error con­verter 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);

That’s it! Feel free to com­ment here or ask ques­tions on our com­mu­nity chan­nels.

Next post

Vert.x Web API Service Introduction

This blog post teaches you how to use the new module vertx-web-api-service to combine the Web Router and the OpenAPI Router Factory with service proxies.

Read more
Previous post

The RSS reader tutorial (Step 3)

This is the third installment of our Vert.x Cassandra Client tutorial. We will implement the last RSS endpoint serving a list of articles related to a specific channel.

Read more
Related posts

Some Rest with Vert.x

This post is part of the Introduction to Vert.x series. Let’s go a bit further this time and develop a CRUD-ish application

Read more

Real-time bidding with Websockets and Vert.x

The expectations of users for interactivity with web applications have changed over the past few years. Users during bidding in auction no longer want to press the refresh button.

Read more

Building services and APIs with AMQP 1.0

Microservices and APIs are everywhere. Everyone talks about them, presentation slides are full of them ... some people are actually even building them.

Read more