This page details HTTP client.

Dependency

<dependency>
  <groupId>io.yupiik.fusion</groupId>
  <artifactId>fusion-httpclient</artifactId>
  <version>${fusion.version}</version>
</dependency>

Core

Fusion 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 RequestListeners 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, see tracing ), 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 straightforward, 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 enforces a custom user-agent value. It defaults to the chrome one.

ExchangeLogger

This listener enables to force all exchanges to be logged.

CurlLoggerListener

This listener is a sibling of ExchangeLogger but instead of a free-form message it logs each exchange as a ready-to-replay curl command (method, URI, headers and body when payload logging is enabled). When payload logging is enabled the command uses -i and is followed by the raw response formatted like curl -i output (status line, headers, body), errors being logged like curl: <message>.

new CurlLoggerListener(Logger.getLogger(getClass().getName()), false)
TIP
since the listener must read the request body to build the command, it replaces the request publisher with the captured bytes so the request is still sent normally.

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)));

Routing, throttling and retries

  • RoutingHttpClient routes requests to several backend clients based on a predicate (for example by host name),

  • ThrottledHttpClient (and its RateLimitedClient/RateLimiter counterparts) throttles outgoing requests to respect a rate limit,

  • redirects and automatic retries are handled by the underlying java.net.http.HttpClient and can be configured on the delegate.

These decorators are combined by wrapping the client: new ThrottledHttpClient(new RoutingHttpClient(delegate)).

Closing

By default (no closeTimeout set) ExtendedHttpClient.close() only closes the registered hooks and listeners: the underlying transport is left as-is, whatever the JDK version. This preserves the historical behavior and keeps the caller in control of the transport lifecycle.

Set a positive closeTimeout (a java.time.Duration) on the configuration to also close the transport when the ExtendedHttpClient created it itself (no delegate was provided):

final var conf = new ExtendedHttpClientConfiguration().setCloseTimeout(Duration.ofSeconds(2));

close() then waits at most closeTimeout for the client to terminate:

  • on Java 21+, it calls HttpClient.shutdown(), then HttpClient.awaitTermination(closeTimeout); if the timeout elapses without termination, HttpClient.shutdownNow() force-closes the remaining operations,

  • on Java 17, the JDK client exposes no shutdown/termination API, so the close is attempted on the common pool and the wait is abandoned (cancelled) when the timeout elapses. This is a best effort: the JDK selector threads are daemons and stop once the client becomes unreachable.

A null, zero or negative closeTimeout leaves the transport untouched.

When a delegate is provided through setDelegate, ExtendedHttpClient.close() never closes it: the transport lifecycle belongs to the caller (this applies with or without closeTimeout).

TIP

the underlying java.net.http.HttpClient (Java 21+) can still be closed manually through close() even when no closeTimeout is set - but a naive close() can block forever on an idle keep-alive connection, which is exactly what the bounded closeTimeout wait prevents.

Tracing

See the tracing page for the TracingListener enabling Zipkin/OpenTelemetry propagation on outgoing requests.