This page details HTTP server.

Dependency

<dependency>
  <groupId>io.yupiik.fusion</groupId>
  <artifactId>fusion-http-server</artifactId>
  <version>${fusion.version}</version>
</dependency>
IMPORTANT

annotations - design API - are in fusion-build-api and only useful at build time.

TIP

set tomcat-catalina in scope runtime to avoid to show the internal API if you don't need them, it will also avoid letting your IDE complete jakarta.* transitive dependencies of tomcat.

Module split

fusion-http-server now aggregates three finer-grained artifacts, so you can pick the level of detail you need (they share the same api):

  • fusion-http-server-api - the contracts (Request/Response/WebServer/Endpoint) and the server-agnostic helpers, without any engine. A WebServerProvider (ServiceLoader-backed, statically cached and overridable with WebServerProvider.setProvider(...)) is what binds WebServer.of() / Cookie.of() to a server implementation.

  • fusion-http-server-observability - the health/metrics API and the default /health and /metrics endpoints.

  • fusion-http-server-tomcat - the Apache Tomcat implementation (servlet adapter, server wiring and beans), the actual provider used when this artifact is on the classpath.

  • fusion-http-server-jre - a Tomcat-free adapter running the same Endpoint API on the JDK com.sun.net.httpserver.HttpServer, depending only on fusion-http-server-api.

For most users, depending on fusion-http-server (the aggregator below) keeps working unchanged.

Usage

By default, if you use Fusion IoC, the webservice will be started. You can customize the configuration listening for WebServer.Configuration event.

Defining an endpoint can be done creating an Endpoint bean and implementing the matcher (matches) and handler which will return a Response thanks the builder:

@ApplicationScoped
public class Greeting implements Endpoint {
    @Override
    public boolean matches(final Request request) {
        return "GET".equals(request.method());
    }

    @Override
    public CompletionStage<Response> handle(final Request request) {
        return completedFuture(Response.of()
                .body("{\"hello\":true}")
                .build());
    }
}
TIP

for byte oriented payloads (JSON typically) prefer Response.of().bytesBody(stream -> ...) which writes directly on the response OutputStream (no container char conversion), and read request bodies through request.unwrapOrNull(InputStream.class) (preferred over Reader.class for the same reason) - this is what the generated JSON endpoints and the JSON-RPC endpoint do.

Configuration

Most of the configuration can be customized using an event listener on WebServer.Configuration and unwrapping the instance as a TomcatWebServerConfiguration you have access to a full Tomcat server customization (HTTP/2.0, WebSocket and so on).

However, there are a few system properties/environment variables (uppercased and with underscores instead of dots) you can set:

  • To skip the initialization of the server at startup: fusion.http-server.start=[true|false]. This can be useful to not start the server in tests for example,

  • To set the HTTP port to use: fusion.http-server.port=<port>, note that setting 0 will make the port random and you can inject WebServer.Configuration to read its value,

  • To set the host to use: fusion.http-server.host=<host>,

  • To set the access log pattern to use: fusion.http-server.accessLogPattern=<...>, see Tomcat documentation for pattern details,

  • To set the webapp directory: fusion.http-server.base=/path/to/www. It can be useful to serve static websites if you configure the right servlets,

  • To set the fusion default servlet mapping: fusion.http-server.fusionServletMapping=/. It can be useful if you want to bind it to a subcontext to use standard servlets for other things like serving base directory,

  • To set if UTF-8 is enforced (default) or not over the requests/responses: fusion.http-server.utf8Setup=true.

Finally, you can also set by default the deployment of a monitoring server (for health checks or metrics depending your application). This is done setting fusion.http-server.monitoring.enabled to true, optionally fusion.http-server.monitoring.port to something else than 8081. The deployed endpoints in this context will be the MonitoringEndpoint bean instances.

The fusion-http-server module directly provides the classic health and metrics endpoints as MonitoringEndpoint beans, in the API package io.yupiik.fusion.http.server.observability:

  • a /health endpoint backed by the HealthRegistry/HealthCheck API,

  • a /metrics endpoint rendering a MetricsRegistry in OpenMetrics format.

Since it is a separate server (an internal port in your cluster/infrastructure), you do not need to secure it - and some Kubernetes tooling (prometheus for ex.) does not like adding headers to gather the data, so it is a good compromise.

Health checks

A health check implements io.yupiik.fusion.http.server.observability.HealthCheck.

The monitoring server exposes a /health endpoint returning an HTTP 200 if all health checks are successful and an HTTP 503 if there is at least one failure.

To distinguish between health check types, implement type() returning something other than the default live. Then call /health?type=<my type> - for example /health?type=live - to run only those checks. Without the type query parameter, all checks are executed.

Metrics

The /metrics endpoint renders the openmetrics stored in io.yupiik.fusion.http.server.observability.MetricsRegistry. It supports Gauge and Counter metric types.

Here is a common way to use it in your application:

@DefaultScoped
public class MyService {
  private final LongAdder myCounter;

  public MyService(final MetricsRegistry registry) {
    this.myCounter = registry.registerCounter("my-service-counter");
  }

  public void save(final MyEntity entity) {
    // do the normal business code
    myCounter.increment();
  }
}
TIP

you can unregister your counter on the registry if it is a short live counter (to use with a session for example), it will then no more be available but using that with prometheus, you have no guarantee it will be polled, so it can be neat to delay the un-registration until next polling. Gauge can make it easy using registerReadOnlyGauge since you then pass a LongSupplier you control.

Here is a sample MonitoringEndpoint (a custom endpoint - the module already provides a default /health backed by HealthRegistry):

@DefaultScoped
public class DatabaseProbe implements MonitoringEndpoint {
    private final TransactionManager transactionManager;

    public DatabaseProbe(final TransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }

    @Override
    public boolean matches(final Request request) {
        return "GET".equalsIgnoreCase(request.method()) && "/health/db".equalsIgnoreCase(request.path());
    }

    @Override
    public CompletionStage<Response> unsafeHandle(final Request request) {
        return transactionManager
                .read(c -> {
                    try {
                        if (!c.isValid(30_000)) {
                            return Optional.of(fail("Timeout"));
                        }
                        return Optional.<CompletionStage<Response>>empty();
                    } catch (final SQLException e) {
                        return Optional.of(fail(e.getMessage()));
                    }
                })
                .orElseGet(() -> completedStage(Response.of().body("OK").build()));
    }
}

High level API

The first option to define an endpoint as a bean - automatically picked - is to use Endpoint.of API:

@Bean
public Endpoint myGreetingEndpoint() {
    return Endpoint.of(
            // matching impl
            request -> "GET".equals(request.method()) &&
                request.path().startsWith("/greet") &&
                request.query() != null &&
                request.query().startsWith("name="),
            // endpoint impl - can be delegated to any bean
            request -> completableStage(Response.of()
                .status(200)
                .header("content-type", "application/json")
                .body(jsonMapper.toString(
                        new Greet(request.query().substring("name=".length()))))
                .build()));
}

The alternative is to use @HttpMatcher API:

@HttpMatcher(method = "GET", pathMatching = EXACT, path = "/greet")
public CompletionStage<Response> myGreetingEndpoint() {
    return completableStage(Response.of()
        .status(200)
        .header("content-type", "application/json")
        .body(jsonMapper.toString(
                new Greet(request.query().substring("name=".length()))))
        .build());
}
TIP

if your endpoint is fully synchronous you can drop the CompletionStage wrapper: public CompletionStage<Response> myGreetingEndpoint();. You can also pass as first parameter a Request parameter.

Tracing

fusion-tracing module provides a Tomcat valve you can set up on your web container to add tracing capabilities - see the tracing page.