Skip to content

Commit

Permalink
Future expectation with HTTP response expectation.
Browse files Browse the repository at this point in the history
  • Loading branch information
vietj committed May 7, 2024
1 parent 65eb104 commit 61803e1
Show file tree
Hide file tree
Showing 12 changed files with 1,285 additions and 58 deletions.
74 changes: 70 additions & 4 deletions src/main/asciidoc/http.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1252,15 +1252,15 @@ The client interface is very simple and follows this pattern:
3. handle the beginning of the {@link io.vertx.core.http.HttpClientResponse}
4. process the response events

You can use Vert.x future composition methods to make your code simpler, however the API is event driven
and you need to understand it otherwise you might experience possible data races (i.e loosing events
You can use Vert.x future composition methods to make your code simpler, however the API is event driven,
and you need to understand it otherwise you might experience possible data races (i.e. loosing events
leading to corrupted data).

NOTE: https://vertx.io/docs/vertx-web-client/java/[Vert.x Web Client] is a higher level API alternative (in fact it is built
on top of this client) you might consider if this client is too low level for your use cases

The client API intentionally does not return a `Future<HttpClientResponse>` because setting a completion
handler on the future can be racy when this is set outside of the event-loop.
handler on the future can be racy when this is set outside the event-loop.

[source,$lang]
----
Expand All @@ -1284,13 +1284,20 @@ vertx.deployVerticle(() -> new AbstractVerticle() {
----

When you are interacting with the client possibly outside a verticle then you can safely perform
composition as long as you do not delay the response events, e.g processing directly the response on the event-loop.
composition as long as you do not delay the response events, e.g. processing directly the response on the event-loop.

[source,$lang]
----
{@link examples.HTTPExamples#exampleClientComposition03}
----

You can also guard the response body with <<response-expectations,HTTP responses expectations>>.

[source,$lang]
----
{@link examples.HTTPExamples#exampleClientComposition03_}
----

If you need to delay the response processing then you need to `pause` the response or use a `pipe`, this
might be necessary when another asynchronous operation is involved.

Expand All @@ -1299,6 +1306,65 @@ might be necessary when another asynchronous operation is involved.
{@link examples.HTTPExamples#exampleClientComposition04}
----

[[response-expectations]]
==== Response expectations

As seen above, you must perform sanity checks manually after the response is received.

You can trade flexibility for clarity and conciseness using _response expectations_.

{@link io.vertx.core.http.HttpResponseExpectation Response expectations} can guard the control flow when the response does
not match a criteria.

The HTTP Client comes with a set of out of the box predicates ready to use:

[source,$lang]
----
{@link examples.HTTPExamples#usingPredefinedExpectations}
----

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

[source,$lang]
----
{@link examples.HTTPExamples#usingPredicates}
----

==== Predefined expectations

As a convenience, the HTTP Client ships a few predicates for common uses cases .

For status codes, e.g. {@link io.vertx.core.http.HttpResponseExpectation#SC_SUCCESS} to verify that the
response has a `2xx` code, you can also create a custom one:

[source,$lang]
----
{@link examples.HTTPExamples#usingSpecificStatus(io.vertx.core.http.HttpClient,io.vertx.core.http.RequestOptions)}
----

For content types, e.g. {@link io.vertx.core.http.HttpResponseExpectation#JSON} to verify that the
response body contains JSON data, you can also create a custom one:

[source,$lang]
----
{@link examples.HTTPExamples#usingSpecificContentType}
----

Please refer to the {@link io.vertx.core.http.HttpResponseExpectation} documentation for a full list of predefined expectations.

==== Creating custom failures

By default, expectations (including the predefined ones) conveys a simple error message. You can customize the exception class by changing the error converter:

[source,$lang]
----
{@link examples.HTTPExamples#expectationCustomError()}
----

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 an exception that
does not capture the stack trace.

==== Reading cookies from the response

You can retrieve the list of cookies from a response using {@link io.vertx.core.http.HttpClientResponse#cookies()}.
Expand Down
101 changes: 93 additions & 8 deletions src/main/java/examples/HTTPExamples.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,7 @@

import io.netty.handler.codec.compression.GzipOptions;
import io.netty.handler.codec.compression.StandardCompressionOptions;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.AsyncResult;
import io.vertx.core.DeploymentOptions;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.MultiMap;
import io.vertx.core.Promise;
import io.vertx.core.Vertx;
import io.vertx.core.*;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.file.AsyncFile;
import io.vertx.core.file.FileSystem;
Expand Down Expand Up @@ -768,6 +761,24 @@ public void exampleClientComposition03(HttpClient client) throws Exception {
});
}

public void exampleClientComposition03_(HttpClient client) throws Exception {

Future<JsonObject> future = client
.request(HttpMethod.GET, "some-uri")
.compose(request -> request
.send()
.expecting(HttpResponseExpectation.SC_OK.and(HttpResponseExpectation.JSON))
.compose(response -> response
.body()
.map(buffer -> buffer.toJsonObject())));
// Listen to the composed final json result
future.onSuccess(json -> {
System.out.println("Received json result " + json);
}).onFailure(err -> {
System.out.println("Something went wrong " + err.getMessage());
});
}

public void exampleClientComposition04(HttpClient client, FileSystem fileSystem) throws Exception {

Future<Void> future = client
Expand All @@ -792,6 +803,80 @@ public void exampleClientComposition04(HttpClient client, FileSystem fileSystem)
}));
}

public void usingPredefinedExpectations(HttpClient client, RequestOptions options) {
Future<Buffer> fut = client
.request(options)
.compose(request -> request
.send()
.expecting(HttpResponseExpectation.SC_SUCCESS)
.compose(response -> response.body()));
}

public void usingPredicates(HttpClient client) {

// Check CORS header allowing to do POST
HttpResponseExpectation methodsPredicate =
resp -> {
String methods = resp.getHeader("Access-Control-Allow-Methods");
return methods != null && methods.contains("POST");
};

// Send pre-flight CORS request
client
.request(new RequestOptions()
.setMethod(HttpMethod.OPTIONS)
.setPort(8080)
.setHost("myserver.mycompany.com")
.setURI("/some-uri")
.putHeader("Origin", "Server-b.com")
.putHeader("Access-Control-Request-Method", "POST"))
.compose(request -> request
.send()
.expecting(methodsPredicate))
.onSuccess(res -> {
// Process the POST request now
})
.onFailure(err ->
System.out.println("Something went wrong " + err.getMessage()));
}

public void usingSpecificStatus(HttpClient client, RequestOptions options) {
client
.request(options)
.compose(request -> request
.send()
.expecting(HttpResponseExpectation.status(200, 202)))
.onSuccess(res -> {
// ....
});
}

public void usingSpecificContentType(HttpClient client, RequestOptions options) {
client
.request(options)
.compose(request -> request
.send()
.expecting(HttpResponseExpectation.contentType("some/content-type")))
.onSuccess(res -> {
// ....
});
}

public void expectationCustomError() {
Expectation<HttpResponseHead> expectation = HttpResponseExpectation.SC_SUCCESS
.wrappingFailure((resp, err) -> new MyCustomException(resp.statusCode(), err.getMessage()));
}

private static class MyCustomException extends Exception {

private final int code;

public MyCustomException(int code, String message) {
super(message);
this.code = code;
}
}

public void exampleFollowRedirect01(HttpClient client) {
client
.request(HttpMethod.GET, "some-uri")
Expand Down
124 changes: 124 additions & 0 deletions src/main/java/io/vertx/core/Expectation.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Copyright (c) 2011-2019 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
*
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
*/
package io.vertx.core;

import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;

/**
* An expectation, very much like a predicate with the ability to provide a meaningful description of the failure.
* <p/>
* Expectation can be used with {@link Future#expecting(Expectation)}.
* @author <a href="mailto:julien@julienviet.com">Julien Viet</a>
*/
@FunctionalInterface
public interface Expectation<V> {

/**
* Check the {@code value}, this should be side effect free, the expectation should either succeed returning {@code true} or fail
* returning {@code false}.
*
* @param value the checked value
*/
boolean test(V value);

/**
* Returned an expectation succeeding when this expectation and the {@code other} expectation succeeds.
*
* @param other the other expectation
* @return an expectation that is a logical and of this and {@code other}
*/
default Expectation<V> and(Expectation<? super V> other) {
Objects.requireNonNull(other);
return new Expectation<>() {
@Override
public boolean test(V value) {
return Expectation.this.test(value) && other.test(value);
}
@Override
public Throwable describe(V value) {
if (!Expectation.this.test(value)) {
return Expectation.this.describe(value);
} else if (!other.test(value)) {
return other.describe(value);
}
return null;
}
};
}

/**
* Returned an expectation succeeding when this expectation or the {@code other} expectation succeeds.
*
* @param other the other expectation
* @return an expectation that is a logical or of this and {@code other}
*/
default Expectation<V> or(Expectation<? super V> other) {
Objects.requireNonNull(other);
return new Expectation<>() {
@Override
public boolean test(V value) {
return Expectation.this.test(value) || other.test(value);
}
@Override
public Throwable describe(V value) {
if (Expectation.this.test(value)) {
return null;
} else if (other.test(value)) {
return null;
} else {
return Expectation.this.describe(value);
}
}
};
}

/**
* Turn an invalid {@code value} into an exception describing the failure, the default implementation returns a generic exception.
*
* @param value the value to describe
* @return a meaningful exception
*/
default Throwable describe(V value) {
return new VertxException("Unexpected result: " + value, true);
}

/**
* Returns a new expectation with the same predicate and a customized error {@code descriptor}.
*
* @param descriptor the function describing the error
* @return a new expectation describing the error with {@code descriptor}
*/
default Expectation<V> wrappingFailure(BiFunction<V, Throwable, Throwable> descriptor) {
class CustomizedExpectation implements Expectation<V> {
private final BiFunction<V, Throwable, Throwable> descriptor;
private CustomizedExpectation(BiFunction<V, Throwable, Throwable> descriptor) {
this.descriptor = Objects.requireNonNull(descriptor);
}
@Override
public boolean test(V value) {
return Expectation.this.test(value);
}
@Override
public Throwable describe(V value) {
Throwable err = Expectation.this.describe(value);
return descriptor.apply(value, err);
}
@Override
public Expectation<V> wrappingFailure(BiFunction<V, Throwable, Throwable> descriptor) {
return new CustomizedExpectation(descriptor);
}
}
return new CustomizedExpectation(descriptor);
}
}
20 changes: 20 additions & 0 deletions src/main/java/io/vertx/core/Future.java
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,26 @@ default Future<T> andThen(Handler<AsyncResult<T>> handler) {
});
}

/**
* Guard the control flow of this future with an expectation.
* <p/>
* When the future is completed with a success, the {@code expectation} is called with the result, when the expectation
* returns {@code false} the returned future is failed, otherwise the future is completed with the same result.
* <p/>
* Expectations are usually lambda expressions:
* <pre>
* return future.expecting(response -> response.statusCode() == 200);
* </pre>
* {@link Expectation} instances can also be used:
* <pre>
* future = future.expecting(HttpResponseExpectation.SC_OK);
* </pre>
*
* @param expectation the expectation
* @return a future succeeded with the result or failed when the expectation returns false
*/
Future<T> expecting(Expectation<? super T> expectation);

/**
* Returns a future succeeded or failed with the outcome of this future when it happens before the timeout fires. When
* the timeout fires before, the future is failed with a {@link java.util.concurrent.TimeoutException}, guaranteeing
Expand Down

0 comments on commit 61803e1

Please sign in to comment.