HTTP Client

UShip HTTP Client is based on java.net.http.HttpClient. It is configured with ExtendedHttpClientConfiguration which enables to:

  • optionally provide a configured HttpClient instance - otherwise the default JVM one is used,

  • optionally provide a set of `RequestListener`s which listen for requests.

Request Listeners

Request listener is a callback triggered before and after each request. It enables to store data before the requests in the caller context and execute some callback once the request is finished. To pass data between both steps (since the request can be asynchronous or not) it uses a State which can hold any data the listener needs.

Tip
if you write custom listeners (to add OpenTracing capabilities for example), you can make them implement AutoCloseable and when closing the HTTP client the method will be called automatically.

Default Listeners

DefaultTimeout

This listener is pretty straight forward, if the request does not have a timeout, it sets it to the default one configured in the listener. It enables to enforce a global timeout to all requests.

SetUserAgent

This listener enforce a custom user-agent value. It defaults to chrome one.

ExchangeLogger

This listener enables to force all exchanges to be logged.

FilteringListener

This listener wraps another listener to filter the calls to before/after callbacks either based on the request or on the response. Can be useful to ignore some data (for example to only capture errors in HARDumperListener).

Tip
for ignoredPaths you can use the syntax regex:<java regex> to match more than an exact path at once.

HARDumperListener

This listener enables to capture a HAR dump of all exchanges which went through the client. It can be very useful to generate some test data you replay with a HAR server in a test or a demo environment.

It comes with its companion HARHttpClient which enables to replay a HAR without actually doing the requests.

Tip
a sibling listener called NDJSONDumperListener exists and allows to log each entry in a ND-JSON output (better in case of error but not standard). It can be combined to NDJSONHttpClient to replay captured requests (in order).
Important
the HttpClient companions of these "capture" listeners must be used in sequential order (until you know you can parallelize them all) because there is no matching logic as of today of requests/responses to enable a wider reuse of captures.

Sample usage

final var conf = new ExtendedHttpClientConfiguration();

// (optional) force a custom SSL context
conf.setDelegate(
    HttpClient.newBuilder()
        .sslContext(getSSLContext())
        .build());

// (optional) force custom listeners
conf.setListeners(List.of(
        new AutoTimeoutSetter(Duration.ofSeconds(3600)),
        new AutoUserAgentSetter(),
        new ExchangeLogger(
            Logger.getLogger(getClass().getName()),
            Clock.systemUTC(),
            false)));

(Open) Tracing

Important
this is not in the same module, you must add tracing module to get this feature.

tracing module provides a Tomcat valve you can set up on your web container to add tracing capabilities to your Tomcat:

final var listener = new TracingListener(
    new ClientTracingConfiguration(), (1)
    accumulator, (2)
    new IdGenerator(IdGenerator.Type.HEX), (3)
    new ServletContextAttributeEvaluator(servletRequest), (4)
    systemUTC()); (5)

(6)
final var configuration = new ExtendedHttpClientConfiguration()
    .setRequestListeners(List.of(listener));
final var client = new ExtendedHttpClient(configuration);
  1. The configuration enables to customize the span tags and headers to enrich the request with,

  2. The accumulator is what will send/log/…​ the spans once aggregated, ensure to configure it as needed,

  3. The IdGenerator provides the span/trace identifiers, it must be compatible with your collector (hex for zipkin for example),

  4. Actually a Supplier<Span>, the span evaluator enables to get parent span, here from the request in a webserver-tomcat using Tracingvalve but any evaluation will work,

  5. The clock enables to timestamp the span and compute its duration,

  6. Finally, add the listener to your http client configuration and create your client.

Important
the accumulator should generally be closed if you reuse AccumulatingSpanCollector. You can combine it with ZipkinFlusher to flush to a zipkin collector v2.

Kubernetes client

kubernetes-client modules providers a HTTP Client already configured for Kubernetes in cluster connection (from a POD). This will typically be used from an operator or cloud native application to call Kubernetes API using a plain and very light HTTP Client from the JVM.

Tip
indeed you can combine it with the enhanced HTTP Client configuring it in the KubernetesClientConfiguration. However, it is recommended to do it using setClientWrapper on the configuration and pass the automatically created client to ExtendedHttpClientConfiguration.setDelegate to avoid to have to handle the SSLContext yourself.

Usage:

final var conf = new KubernetesClientConfiguration()
    .setClientWrapper(client -> new ExtendedHttpClient(new ExtendedHttpClientConfiguration()
        .setDelegate(client)));
final var k8s = new KubernetesClient(conf);

// now call any API you need:
final var response = k8s.send(
        HttpRequest.newBuilder()
                .GET()
                .uri(URI.create(
                    "https://kubernetes.api/api/v1/namespaces/" + k8s.namespace().orElse("default") + "/configmaps?" +
                            "includeUninitialized=false&" +
                            "limit=1000&" +
                            "timeoutSeconds=600")
                .header("Accept", "application/json")
                .build(),
        HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
// handle the response
Important
as you can see, there is no need to pass the token to the request, it is done under the hood by the KubernetesClient. The other important note is that https://kubernetes.api is automatically replaced by the conf.getMaster() value. This enables your code to stay more straight forward in general but if you pass them, the client will handle it properly too.

JSON-RPC client

As a JSON-RPC server companion UShip also provides a JSON-RPC client.

<dependency>
  <groupId>io.yupiik.uship</groupId>
  <artifactId>jsonrpc-client</artifactId>
  <version>${uship.version}</version>
</dependency>

The minimum configuration is to provide the JSON-RPC endpoint:

final var client = new JsonRpcClient(new JsonRpcClientConfiguration("http://app.service.com/jsonrpc"));

But more is customizable in JsonRpcClientConfiguration and a common initialization would look like:

@Produces
@ApplicationScoped
public ExtendedHttpClient jsonRpcHttpClient() {
    return ...; // create it as usual
}

public void releaseHttpClient(@Disposes final ExtendedHttpClient client) {
    client.close();
}

@Produces
@ApplicationScoped
public JsonRpcClient jsonRpcClient(
            // external config
            @ConfigProperty("jsonrpc.base") final String base,
            @ConfigProperty("jsonrpc.authorizationheader") final String auth,
            // from uship json in general
            final JsonBuilderFactory jbf, final Jsonb jsonb,
            // created in the app (other producer)
            final ExtendedHttpClient client) {
    return new JsonRpcClient(new JsonRpcClientConfiguration(base + "/jsonrpc"))
        // all these setters are optional but enables to use controlled instances (vs implicit) and optimize/customize the behavior/mem/security
        .setJsonBuilderFactory(jbf)
        .setJsonb(jsonb)
        .setHttpClient(client) // enables to customize the async thread pool for example (highly encourage for executeAsync usage)
        .setRequestCustomizer(req -> req.header("authorization", auth));
}

@Produces
@ApplicationScoped // to be able to @Inject JsonRpcClient.Sync sync; directory - or similarly for async
public JsonRpcClient.Sync jsonRpcClient(final JsonRpcClient root) {
    return root.sync();
}

public void releaseJsonRpcClient(@Disposes final JsonRpcClient client) {
    client.close();
}

Then you can simply use it in your application:

@ApplicationScoped
public class MyService {
    @Inject
    private JsonRpcClient client;

    public CompletionStage<Customer> findCustomer(final String id) {
        return client.async().execute("app-customer-find-by-id", Map.of("id", id))
            // todo: better error handling if needed
            .thenApply(r -> r.asSingle().as(Customer.class));
    }
}

Bulk handling

Bulk is handled relying on the JSON-RPC protocol accessible from the client - or directly if you prefer:

final var protocol = client.protocol();
final var response = client.sync().execute(protocol.jsonBuilderFactory().createArrayBuilder()
        .add(protocol.toJsonRpcRequest("m1", Map.of("foo", "bar")))
        .add(protocol.toJsonRpcRequest("m2", new M2Params()))
        .add(protocol.toJsonRpcRequest("m3", null))
        .build());