Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Architecture

Design

MCP Rotuer

Implementing an MCP (Model Context Protocol) Router inside light-4j gateway is a visionary and highly strategic idea.

The industry is currently struggling with “Tool Sprawl”—where every microservice needs to be manually taught to an LLM. By placing an MCP Router at the gateway level, we effectively turn our entire microservice ecosystem into a single, searchable library of capabilities for AI agents.

Here is a breakdown of why this is a good idea and how to design it for the light-4j ecosystem.


1. Why it is a Strategic Win

  • Discovery at Scale: Instead of configuring 50 tools for an agent, the agent connects to our gateway via MCP. The gateway then “advertises” the available tools based on the services it already knows.
  • Protocol Translation: Our backend services don’t need to know what MCP is. The gateway handles the conversion from MCP (JSON-RPC over SSE/HTTP) to REST (OpenAPI) or GraphQL.
  • Security & Governance: We can apply light-4j’s existing JWT validation, rate limiting, and audit logging to AI interactions. We control which agents have access to which “tools” (APIs).
  • Schema Re-use: We already have OpenAPI specs or GraphQL schemas in light-4j. We can dynamically generate the MCP “Tool Definitions” from these existing schemas.

2. Design Considerations

A. The Transport Layer

MCP supports two primary transports: Stdio (for local scripts) and HTTP with SSE (Server-Sent Events) (for remote services).

  • Decision: For a gateway, we must use the HTTP + SSE transport.
  • Implementation: Light-4j is built on Undertow, which has excellent support for SSE. We will need to implement an MCP endpoint (e.g., /mcp/message) and an SSE endpoint (e.g., /mcp/sse).

B. Tool Discovery & Dynamic Mapping

How does the Gateway decide which APIs to expose as MCP Tools?

  • Metadata Driven: Use light-4j configuration or annotations in the OpenAPI files to mark specific endpoints as “AI-enabled.”
  • The Mapper: Create a component that converts an OpenAPI Operation into an MCP Tool Definition.
    • description in OpenAPI becomes the tool’s description (crucial for LLM reasoning).
    • requestBody schema becomes the tool’s inputSchema.

C. Authentication & Context Pass-through

This is the hardest part.

  • The Problem: The LLM agent connects to the Gateway, but the Backend Microservice needs a user-specific JWT.
  • The Solution: The MCP Router must be able to take the identity from the MCP connection (initial handshake) and either pass it through or exchange it for a backend token (OAuth2 Token Exchange).

D. Statefulness vs. Statelessness

MCP is often stateful (sessions).

  • Implementation: Since light-4j is designed for high performance and statelessness, we may need light-session-4j a small Session Manager (potentially backed by Redis or PostgresQL) to keep track of which MCP Client is mapped to which internal context during the SSE connection.

3. Implementation Plan for light-4j

Step 1: Create the McpHandler

Create a new middleware handler in light-4j that intercepts calls to /mcp.

  • This handler must implement the MCP lifecycle: initialize -> list_tools -> call_tool.

Step 2: Tool Registry

Implement a registry that scans our gateway’s internal routing table.

  • REST: For every path (e.g., GET /customers/{id}), generate an MCP tool named get_customers_by_id.
  • GraphQL: For every Query/Mutation, generate a corresponding MCP tool.

Step 3: JSON-RPC over SSE

MCP uses JSON-RPC 2.0. We will need a simple parser that:

  1. Receives an MCP call_tool request.
  2. Identifies the internal REST/GraphQL route.
  3. Executes a local dispatch (internal call) to the existing light-4j handler for that service.
  4. Wraps the response in an MCP content object and sends it back via SSE.

Step 4: Governance (The “Agentic” Layer)

Add a “Critique” or “Guardrail” check. Since this is at the gateway, we can inspect the tool output. If the LLM requested sensitive data, the Gateway can mask it before the agent sees it.


4. Potential Challenges to Watch

  1. Context Window Overload: If our gateway has 500 APIs, sending 500 tool definitions to the LLM will crash its context window.
    • Solution: Implement Categorization. When an agent connects, it should specify a “Scope” (e.g., mcp?scope=accounting), and the gateway only returns tools relevant to that scope.
  2. Latency: Adding a protocol translation layer at the gateway adds milliseconds.
    • Solution: Light-4j’s native performance is our advantage here. Minimize JSON serialization/deserialization by using direct buffer access where possible.
  3. Complex Schemas: Some API payloads are too complex for an LLM to understand.
    • Solution: Provide a “Summary” view. Allow our MCP router to transform a complex 100-field JSON response into a 5-field summary that the LLM can actually use.

Conclusion

Building an MCP Router for light-4j transforms our API Gateway from a “Traffic Cop” into an “AI Brain Center.” It allows our Java-based enterprise services to be “Agent-Ready” without touching the underlying microservice code.

Configuration Hot Reload Design

Introduction

In the light-4j framework, minimizing downtime is crucial for microservices. The Configuration Hot Reload feature allows services to update their configuration at runtime without restarting the server. This design document outlines the centralized caching architecture used to achieve consistent and efficient hot reloads.

Architecture Evolution

Previous Approach (Decentralized)

Initially, configuration reload was handled in a decentralized manner:

  • Each handler maintained its own static configuration object.
  • A reload() method was required on every handler to manually refresh this object.
  • The ConfigReloadHandler used reflection to search for and invoke these reload() methods.

Drawbacks:

  • Inconsistency: Different parts of the application could hold different versions of the configuration.
  • Complexity: Every handler needed boilerplate code for reloading.
  • State Management: Singleton classes (like ClientConfig) often held stale references that were difficult to update.

Current Approach (Centralized Cache)

The new architecture centralizes the “source of truth” within the Config class itself.

  • Centralized Cache: The Config class maintains a ConcurrentHashMap of all loaded configurations.
  • Cache Invalidation: Instead of notifying components to reload, we simply invalidate the specific entry in the central cache.
  • Lazy Loading: Consumers (Handlers, Managers) fetch the configuration from the Config class at the moment of use. If the cache is empty (cleared), Config reloads it from the source files.

Detailed Design

1. The Config Core (Config.java)

The Config class is enhanced to support targeted cache invalidation.

public abstract class Config {
    // ... existing methods
    
    // New method to remove a specific config from memory
    public abstract void clearConfigCache(String configName);
}

When clearConfigCache("my-config") is called:

  1. The entry for “my-config” is removed from the internal configCache.
  2. The next time getJsonMapConfig("my-config") or getJsonObjectConfig(...) is called, the Config class detects the miss and reloads the file from the filesystem or external config server.

2. The Admin Endpoint (ConfigReloadHandler)

The ConfigReloadHandler exposes the /adm/config-reload endpoint. Its responsibility has been simplified:

  1. Receive Request: Accepts a list of modules/plugins to reload.
  2. Resolve Config Names: Looks up the configuration file names associated with the requested classes using the ModuleRegistry.
  3. Invalidate Cache: Calls Config.getInstance().clearConfigCache(configName) for each identified module.

It no longer relies on reflection to call methods on the handlers.

3. Configuration Consumers

Handlers and other components must follow a stateless pattern regarding configuration.

Anti-Pattern (Old Way):

public class MyHandler {
    static MyConfig config = MyConfig.load(); // Static load at startup
    
    public void handleRequest(...) {
        // Use static config - will remain stale even if file changes
        if (config.isEnabled()) ... 
    }
}

Recommended Pattern (New Way):

public class MyHandler {
    // No static config field
    
    public void handleRequest(...) {
        // Fetch fresh config from central cache every time
        // This is fast due to HashMap lookup
        MyConfig config = MyConfig.load(); 
        
        if (config.isEnabled()) ...
    }
}

Implementation in Config Classes: The load() method in configuration classes (e.g., CorrelationConfig) simply delegates to the cached Config methods:

private CorrelationConfig(String configName) {
    // Always ask Config class for the Map. 
    // If cache was cleared, this call triggers a file reload.
    mappedConfig = Config.getInstance().getJsonMapConfig(configName); 
}

4. Handling Singletons (e.g., ClientConfig)

For Singleton classes that parse configuration into complex objects, they must check if the underlying configuration has changed.

public static ClientConfig get() {
    // Check if the Map instance in Config.java is different from what we typically hold
    Map<String, Object> currentMap = Config.getInstance().getJsonMapConfig(CONFIG_NAME);
    if (instance == null || instance.mappedConfig != currentMap) {
        synchronized (ClientConfig.class) {
            if (instance == null || instance.mappedConfig != currentMap) {
                instance = new ClientConfig(); // Re-parse and create new instance
            }
        }
    }
    return instance;
}
}
return instance;

}

5. Lazy Rebuild on Config Change

Some handlers (like LightProxyHandler) maintain expensive internal objects that depend on the configuration (e.g., LoadBalancingProxyClient, ProxyHandler). Recreating these on every request is not feasible due to performance. However, they must still react to configuration changes.

For these cases, we use a Lazy Rebuild pattern:

  1. Volatile Config Reference: The handler maintains a volatile reference to its configuration object.
  2. Check on Request: At the start of handleRequest, it checks if the cached config object is the same as the one returned by Config.load().
  3. Rebuild if Changed: If the reference has changed (identity check), it synchronizes and rebuilds the internal components.

Example Implementation (LightProxyHandler):

public class LightProxyHandler implements HttpHandler {
    private volatile ProxyConfig config;
    private volatile ProxyHandler proxyHandler;

    public LightProxyHandler() {
        this.config = ProxyConfig.load();
        buildProxy(); // Initial build
    }

    private void buildProxy() {
        // Expensive object creation based on config
        this.proxyHandler = ProxyHandler.builder()
                .setProxyClient(new LoadBalancingProxyClient()...)
                .build();
    }

    @Override
    public void handleRequest(HttpServerExchange exchange) throws Exception {
        ProxyConfig newConfig = ProxyConfig.load();
        // Identity check: ultra-fast
        if (newConfig != config) {
            synchronized (this) {
                newConfig = ProxyConfig.load(); // Double-check
                if (newConfig != config) {
                    config = newConfig;
                    buildProxy(); // Rebuild internal components
                }
            }
        }
        // Use the (potentially new) proxyHandler
        proxyHandler.handleRequest(exchange);
    }
}

This pattern ensures safe updates without the overhead of rebuilding on every request, and without requiring a manual reload() method.

6. Config Class Implementation Pattern (Singleton with Caching)

For configuration classes that are frequently accessed (per request), instantiating a new object each time can be expensive. We recommend implementing a Singleton pattern that caches the configuration object and only invalidates it when the underlying configuration map changes.

Example Implementation (ApiKeyConfig):

public class ApiKeyConfig {
    private static final String CONFIG_NAME = "apikey";
    // Cache the instance
    private static ApiKeyConfig instance;
    private final Map<String, Object> mappedConfig;
    
    // Private constructor to force use of load()
    private ApiKeyConfig(String configName) {
        mappedConfig = Config.getInstance().getJsonMapConfig(configName);
        setConfigData();
    }

    public static ApiKeyConfig load() {
        return load(CONFIG_NAME);
    }

    public static ApiKeyConfig load(String configName) {
        // optimistically check if we have a valid cached instance
        Map<String, Object> mappedConfig = Config.getInstance().getJsonMapConfig(configName);
        if (instance != null && instance.getMappedConfig() == mappedConfig) {
            return instance;
        }
        
        // Double-checked locking for thread safety
        synchronized (ApiKeyConfig.class) {
            mappedConfig = Config.getInstance().getJsonMapConfig(configName);
            if (instance != null && instance.getMappedConfig() == mappedConfig) {
                return instance;
            }
            instance = new ApiKeyConfig(configName);
            // Register the module with the configuration. masking the apiKey property.
            // As apiKeys are in the config file, we need to mask them.
            List<String> masks = new ArrayList<>();
            // if hashEnabled, there is no need to mask in the first place.
            if(!instance.hashEnabled) {
                masks.add("apiKey");
            }
            ModuleRegistry.registerModule(configName, ApiKeyConfig.class.getName(), Config.getNoneDecryptedInstance().getJsonMapConfigNoCache(configName), masks);

            return instance;
        }
    }
    
    public Map<String, Object> getMappedConfig() {
        return mappedConfig;
    }
}

This pattern ensures that:

  1. Performance: Applications use the cached instance for the majority of requests (fast reference check).
  2. Freshness: If Config.getInstance().getJsonMapConfig(name) returns a new Map object (due to a reload), the equality check fails, and a new ApiKeyConfig is created.
  3. Consistency: The handleRequest method still calls ApiKeyConfig.load(), but receives the singleton instance transparently.

6. Thread Safety

The Config class handles concurrent access using the Double-Checked Locking pattern to ensure that the configuration file is loaded exactly once, even if multiple threads request it simultaneously immediately after the cache is cleared.

Scenario:

  1. Thread A and Thread B both handle a request for MyHandler.
  2. Both call MyConfig.load(), which calls Config.getJsonMapConfig("my-config").
  3. Both see that the cache is empty (returning null) because it was just cleared by the reload handler.
  4. Thread A acquires the lock (synchronized). Thread B waits.
  5. Thread A checks the cache again (still null), loads the file from disk, puts it in the configCache, and releases the lock.
  6. Thread B acquires the lock.
  7. Thread B checks the cache again. This time it finds the config loaded by Thread A.
  8. Thread B uses the existing config without loading from disk.

This ensures no race conditions or redundant file I/O operations occur in high-concurrency environments.

Workflow Summary

  1. Update: User updates values.yml or a config file on the server/filesystem.
  2. Trigger: User calls POST https://host:port/adm/config-reload with the module name.
  3. Clear: ConfigReloadHandler tells Config.java to clearConfigCache for that module.
  4. Processing:
    • Step 4a: Request A arrives at MyHandler.
    • Step 4b: MyHandler calls MyConfig.load().
    • Step 4c: MyConfig calls Config.getJsonMapConfig().
    • Step 4d: Config sees cache miss, reads file from disk, parses it, puts it in cache, and returns it.
    • Step 4e: MyHandler processes request with NEW configuration.
  5. Subsequent Requests: Step 4d is skipped; data is served instantly from memory.

Configuration Consistency During Request Processing

Can Config Objects Change During a Request?

A common question arises: Can the config object created in handleRequest be changed during the request/response exchange?

Short Answer: Theoretically possible but extremely unlikely, and the design handles this correctly.

Understanding the Behavior

When a handler processes a request using the recommended pattern:

public void handleRequest(HttpServerExchange exchange) {
    MyConfig config = MyConfig.load(); // Creates local reference
    
    // Use config throughout request processing
    if (config.isEnabled()) {
        // ... process request
    }
}

The config variable is a local reference to a configuration object. Here’s what happens:

  1. Cache Hit: MyConfig.load() calls Config.getJsonMapConfig(configName) which returns a reference to the cached Map<String, Object>.
  2. Object Construction: A new MyConfig object is created, wrapping this Map reference.
  3. Local Scope: The config variable holds this reference for the duration of the request.

Scenario: Reload During Request Processing

Consider this timeline:

  1. T1: Request A starts, calls MyConfig.load(), gets reference to Config Object v1
  2. T2: Admin calls /adm/config-reload, cache is cleared
  3. T3: Request B starts, calls MyConfig.load(), triggers reload, gets reference to Config Object v2
  4. T4: Request A continues processing with Config Object v1
  5. T5: Request A completes successfully with Config Object v1

Key Points:

  • Request A maintains its reference to the original config object throughout its lifecycle
  • Request B gets a new config object with reloaded values
  • Both requests process correctly with consistent configuration for their entire duration
  • No race conditions or inconsistent state within a single request

Why This Design is Safe

1. Immutable Config Objects

Configuration objects are effectively immutable once constructed:

private MyConfig(String configName) {
    mappedConfig = Config.getInstance().getJsonMapConfig(configName);
    setConfigData(); // Parses and sets final fields
}

Fields are set during construction and never modified afterward.

2. Local Variable Isolation

Each request has its own local config variable:

  • The reference is stored on the thread’s stack
  • Even if the cache is cleared, the reference remains valid
  • The underlying Map object continues to exist until no references remain (garbage collection)

3. Per-Request Consistency

This design ensures that each request has a consistent view of configuration from start to finish:

  • No mid-request configuration changes
  • Predictable behavior throughout request processing
  • Easier debugging and reasoning about request flow

4. Graceful Transition

The architecture enables zero-downtime config updates:

  • In-flight requests: Complete with the config they started with
  • New requests: Use the updated configuration
  • No interruption: No requests fail due to config reload

Edge Cases and Considerations

Long-Running Requests

For requests that take significant time to process (e.g., minutes):

  • The request will complete with the configuration it started with
  • If config is reloaded during processing, the request continues with “old” config
  • This is correct behavior - we want consistent config per request

High-Concurrency Scenarios

During a config reload under heavy load:

  • Multiple threads may simultaneously detect cache miss
  • Double-checked locking ensures only one thread loads from disk
  • All threads eventually get the same new config instance
  • No duplicate file I/O or parsing overhead

Memory Implications

Question: If old config objects are still referenced by in-flight requests, do we have memory leaks?

Answer: No, this is handled by Java’s garbage collection:

  1. Request A holds reference to Config Object v1
  2. Cache is cleared and reloaded with Config Object v2
  3. Request A completes and goes out of scope
  4. Config Object v1 has no more references
  5. Garbage collector reclaims Config Object v1

The memory overhead is minimal and temporary, lasting only as long as the longest in-flight request.

Best Practices

To ensure optimal behavior with hot reload:

  1. Always Load Fresh: Call MyConfig.load() at the start of handleRequest, not in constructor

    // ✅ GOOD
    public void handleRequest(...) {
        MyConfig config = MyConfig.load();
    }
    
    // ❌ BAD
    private static MyConfig config = MyConfig.load();
    
  2. Use Local Variables: Store config in local variables, not instance fields

    // ✅ GOOD
    MyConfig config = MyConfig.load();
    
    // ❌ BAD
    this.config = MyConfig.load();
    
  3. Don’t Cache in Handlers: Let the Config class handle caching

    // ✅ GOOD - Load on each request
    public void handleRequest(...) {
        MyConfig config = MyConfig.load();
    }
    
    // ❌ BAD - Caching in handler
    private MyConfig cachedConfig;
    public void handleRequest(...) {
        if (cachedConfig == null) cachedConfig = MyConfig.load();
    }
    

Summary

The centralized cache design ensures:

  • Thread Safety: Multiple threads can safely reload and access config
  • Request Consistency: Each request has a stable config view from start to finish
  • Zero Downtime: Config updates don’t interrupt in-flight requests
  • Performance: HashMap lookups are extremely fast (O(1))
  • Simplicity: No complex synchronization needed in handlers

Benefits

  1. Performance: Only one disk read per reload cycle. Subsequent accesses are Hash Map lookups.
  2. Reliability: Config state is consistent. No chance of “half-reloaded” application state.
  3. Simplicity: drastic reduction in boilerplate code across the framework.

Module Registry Design

Introduction

The Module Registry is a core component in the light-4j framework that tracks all active modules (middleware handlers, plugins, utilities) and their current configurations. This info is primarily exposed via the /server/info admin endpoint, allowing operators to verify the runtime state of the service.

With the introduction of Centralized Configuration Management and Hot Reload, the Module Registry design has been updated to ensure consistency updates without manual intervention from handlers.

Architecture

Centralized Registration

Previously, each Handler was responsible for registering its configuration in its constructor or reload() method. This led to decentralized logic and potential inconsistencies during hot reloads.

In the new design, Configuration Classes (*Config.java) are responsible for registering themselves with the ModuleRegistry immediately upon instantiation.

Workflow:

  1. Handler requests config: MyConfig.load().
  2. Config class loads data from the central cache.
  3. MyConfig constructor initializes fields.
  4. MyConfig constructor calls ModuleRegistry.registerModule(...).

Caching and Optimization

Since MyConfig objects are instantiated per-request (to ensure fresh config is used), the registration call happens frequently. To prevent performance degradation, ModuleRegistry implements an Identity Cache.

// Key: configName + ":" + moduleClass
private static final Map<String, Object> registryCache = new HashMap<>();

public static void registerModule(String configName, String moduleClass, Map<String, Object> config, List<String> masks) {
    String key = configName + ":" + moduleClass;
    
    // Optimization: Identity Check
    if (config != null && registryCache.get(key) == config) {
        return; // Exact same object already registered, skip overhead
    }
    // ... proceed with registration
}

This ensures that while the registration intent is declared on every request, the heavy lifting (deep copying, masking) only happens when the configuration object actually changes (i.e., after a reload).

Security and Masking

Configurations often contain sensitive secrets (passwords, API keys). The ModuleRegistry must never store or expose these in plain text.

1. Non-Decrypted Config

The Config framework supports auto-decryption of CRYPT:... values. However, server/info should show the original encrypted value (or a mask), not the decrypted secret.

Config classes register the Non-Decrypted version of the config map:

// Inside MyConfig constructor
ModuleRegistry.registerModule(
    CONFIG_NAME, 
    MyConfig.class.getName(), 
    Config.getNoneDecryptedInstance().getJsonMapConfigNoCache(CONFIG_NAME), 
    List.of("secretKey", "password")
);

2. Deep Copy & Masking

To prevent the ModuleRegistry from accidentally modifying the active configuration object (or vice versa), and to safely apply masks without affecting the runtime application:

  1. ModuleRegistry creates a Deep Copy of the configuration map.
  2. Masks (e.g., replacing values with *) are applied to the copy.
  3. The masked copy is stored in the registry for display.

Best Practices for Module Developers

When creating a new module with a configuration file:

  1. Self-Register in Config Constructor: Call ModuleRegistry.registerModule inside your Config class constructor.
  2. Use Non-Decrypted Instance: Always fetch the config for registration using Config.getNoneDecryptedInstance().
  3. Define Masks: specific attributes that should be masked (e.g., passwords, tokens) in the registration call.
  4. Remove Handler Registration: Do not call register in your Handler, static blocks, or reload() methods.

Example

public class MyConfig {
    private MyConfig(String configName) {
        // 1. Load runtime config
        config = Config.getInstance();
        mappedConfig = config.getJsonMapConfig(configName);
        setConfigData();
        
        // 2. Register with ModuleRegistry
        ModuleRegistry.registerModule(
            configName, 
            MyConfig.class.getName(), 
            Config.getNoneDecryptedInstance().getJsonMapConfigNoCache(configName), 
            List.of("clientSecret") // Mask sensitive fields
        );
    }
}

HttpClient Retry

Since the JDK HttpClient supports proxy configuration, we use it to connect to external services through NetSkope or McAfee gateways within an enterprise environment. When comparing it with the light-4j Http2Client, we identified several important behavioral differences and usage considerations.

Unresolved InetSocketAddress

When configuring a proxy for the JDK HttpClient, it is important to use an unresolved socket address for the proxy host.

        if (config.getProxyHost() != null && !config.getProxyHost().isEmpty()) {
            clientBuilder.proxy(ProxySelector.of(
                    InetSocketAddress.createUnresolved(
                            config.getProxyHost(),
                            config.getProxyPort() == 0 ? 443 : config.getProxyPort()
                    )
            ));
        }

Using InetSocketAddress.createUnresolved() ensures that DNS resolution occurs when a new connection is established, rather than at client creation time. This allows DNS lookups to respect TTL values and return updated IP addresses when DNS records change.

Retry with Temporary HttpClient

The JDK HttpClient is a heavyweight, long-lived object and should not be created frequently. It also does not expose APIs to directly manage connections (for example, closing an existing connection and retrying with a fresh one).

A naive retry strategy is to create a new HttpClient instance for retries. Below is an implementation we used for testing purposes:

                    /*
                    A new client is created from the second attempt onwards as there is no way we can create new connections. Since this handler
                    is a singleton, it would be a bad idea to repeatedly abandon HttpClient instances in a singleton handler. It will cause leak
                    of resource. We need a clean connection without abandoning the shared resource. The solution is to create a temporary fresh
                    client from the second attempt and discard it immediately to minimize resource footprint.
                    */
                    int maxRetries = config.getMaxConnectionRetries();
                    HttpResponse<byte[]> response = null;

                    for (int attempt = 0; attempt < maxRetries; attempt++) {

                        try {
                            if (attempt == 0) {
                                // First attempt: Use the long-lived, shared client
                                response = this.client.send(request, HttpResponse.BodyHandlers.ofByteArray());
                            } else {
                                // Subsequent attempts: Create a fresh, temporary client inside a try-with-resources
                                logger.info("Attempt {} failed. Creating fresh client for retry.", attempt);

                                try (HttpClient temporaryClient = createJavaHttpClient()) {
                                    response = temporaryClient.send(request, HttpResponse.BodyHandlers.ofByteArray());
                                } // temporaryClient.close() called here if inner block exits normally/exceptionally
                            }
                            break; // Success! Exit the loop.
                        } catch (IOException | InterruptedException e) {
                            // Note: The exception could be from .send() OR from createJavaHttpClient() (if attempt > 0)
                            if (attempt >= maxRetries - 1) {
                                throw e; // Rethrow exception on final attempt failure
                            }
                            logger.warn("Attempt {} failed ({}). Retrying...", attempt + 1, e.getMessage());
                            // Loop continues to next attempt
                        }
                    }

Load Test Findings

We created a test project under light-example-4j/instance-variable consisting of a caller service and a callee service. The caller created a new HttpClient per request.

During load testing, we observed:

  • Extremely high thread and memory utilization

  • Very poor throughput

  • Sustained 100% CPU usage for several minutes after traffic stopped, as the JVM attempted to clean up HttpClient resources

This behavior exposes several serious problems.

Identified Problems

The following is the problems:

  1. Resource Exhaustion (Threads & Native Memory) The java.net.http.HttpClient is designed to be a long-lived, heavy object. When you create an instance, it spins up a background thread pool (specifically a SelectorManager thread) to handle async I/O. If you create a new client for every HTTP request, you are spawning thousands of threads. Even though the client reference is overwritten and eventually Garbage Collected, the background threads may not shut down immediately, leading to OutOfMemoryError: unable to create new native thread or high CPU usage.

  2. Loss of Connection Pooling (Performance Killer) The HttpClient maintains an internal connection pool to reuse TCP connections (Keep-Alive) to the downstream service (localhost:7002). By recreating the client every time, you force a new TCP handshake (and SSL handshake if using HTTPS) for every single request. This drastically increases latency.

  3. Thread Safety (Race Condition) Your handler is a singleton, meaning one instance of DataGetHandler serves all requests. You have defined private HttpClient client; as an instance variable.

    • Scenario: Request A comes in, creates a client, and assigns it to this.client. Immediately after, Request B comes in and overwrites this.client with a new client.
    • Because handleRequest is called concurrently by multiple Undertow worker threads, having a mutable instance variable shared across requests is unsafe. While it might not crash immediately, it is poor design to share state this way without synchronization (though in this specific logic, you don’t actually need to share the state, which makes it even worse).

Connection: close Header

Given the above findings, creating temporary HttpClient instances for retries is too risky. Further investigation revealed a safer and more efficient approach: forcing a new connection using the Connection: close header.

By disabling persistent connections for a retry request, the existing HttpClient can be reused while ensuring the next request establishes a fresh TCP connection.

Why this is better than creating a new HttpClient

  • Lower Resource Overhead: Recreating HttpClient creates a new SelectorManager thread and internal infrastructure, which is a heavy operation.

  • Prevents Thread Leaks: Repeatedly creating and discarding clients can lead to “SelectorManager” thread buildup if not closed properly.

  • Clean Socket Management: The Connection: close header handles the lifecycle at the protocol level, allowing the existing client’s thread pool to remain stable.

How does the header work

The Connection: close header instructs both the client and the server (or load balancer) to terminate the TCP connection immediately after the current request/response exchange is finished. When you use this header on a retry, the following happens:

  • Current Request: The client established a connection (or reused one) to send the request.

  • After Response: Once the response is received (or the request fails), the client’s internal pool will not return that connection to the pool; it will close the socket.

  • Next Attempt: Any subsequent request made by that same HttpClient instance will be forced to open a brand-new connection. This fresh connection will hit your load balancer again, which can then route it to a different, healthy node.

In most high-availability scenarios, retrying a third time (or using a “3-strikes” policy) is recommended. Here is the optimal sequence:

  • 1st Attempt (Normal): Use the default persistent connection. This is fast and efficient.

  • 2nd Attempt (The “Fresh Connection” Retry): Use the Connection: close header. This forces the load balancer to re-evaluate the target node if the first one was dead or hanging.

  • 3rd Attempt (Final Safeguard): If the second attempt fails, it is often worth one final try with a small exponential backoff (e.g., 500ms–1s). This helps in cases of transient network congestion or when the load balancer hasn’t yet updated its health checks to exclude the failing node.

                    /*
                     * 1st Attempt (Normal): Use the default persistent connection. This is fast and efficient.

                     * 2nd Attempt (The "Fresh Connection" Retry): Use the Connection: close header. This forces
                     *  the load balancer to re-evaluate the target node if the first one was dead or hanging.

                     * 3rd Attempt (Final Safeguard): It will use a new connection to send the request.
                     */
                    int maxRetries = config.getMaxConnectionRetries();
                    HttpResponse<byte[]> response = null;

                    for (int attempt = 0; attempt < maxRetries; attempt++) {
                        try {
                            HttpRequest finalRequest;

                            if (attempt == 0) {
                                // First attempt: Use original request (Keep-Alive enabled by default)
                                finalRequest = request;
                            } else {
                                // Subsequent attempts: Force a fresh connection by adding the 'Connection: close' header.
                                // This ensures the load balancer sees a new TCP handshake and can route to a new node.
                                logger.info("Attempt {} failed. Retrying with 'Connection: close' to force fresh connection.", attempt);

                                finalRequest = HttpRequest.newBuilder(request, (name, value) -> true)
                                        .header("Connection", "close")
                                        .build();
                            }

                            // Always use the same shared, long-lived client
                            response = this.client.send(finalRequest, HttpResponse.BodyHandlers.ofByteArray());
                            break; // Success! Exit the loop.

                        } catch (IOException | InterruptedException e) {
                            if (attempt >= maxRetries - 1) {
                                throw e; // Rethrow on final attempt
                            }
                            logger.warn("Attempt {} failed ({}). Retrying...", attempt + 1, e.getMessage());

                            // Optional: Add a small sleep/backoff here to allow LB health checks to update
                        }
                    }

Header Restriction

Header Restriction: Ensure your JVM is started with -Djdk.httpclient.allowRestrictedHeaders=connection to allow the HttpClient to modify the Connection header.

To make it easier, we have added the following code to the handler.

    static {
        System.setProperty("jdk.httpclient.allowRestrictedHeaders", "host,connection");
    }

Light GenAI Client Design

Introduction

The light-genai-4j library provides a standardized way for Light-4j applications to interact with various Generative AI (GenAI) providers. By abstracting the underlying client implementations behind a common interface, applications can support dynamic model switching and simplified integration for different environments (e.g., local development vs. production).

Architecture

The project is structured into a core module and provider-specific implementation modules.

Modules

  1. genai-core: Defines the common interfaces and shared utilities.
  2. genai-ollama: Implementation for the Ollama API, suitable for local LLM inference.
  3. genai-bedrock: Implementation for AWS Bedrock, suitable for enterprise-grade managed LLMs.

Interface Design

The core interaction is defined by the GenAiClient interface in the genai-core module.

package com.networknt.genai;

import java.util.List;

public interface GenAiClient {
    /**
     * Generates a text completion for the given list of chat messages.
     * 
     * @param messages The list of chat messages (history).
     * @return The generated text response from the model.
     */
    String chat(List<ChatMessage> messages);

    /**
     * Generates a text completion stream for the given list of chat messages.
     * 
     * @param messages The list of chat messages (history).
     * @param callback The callback to receive chunks, completion, and errors.
     */
    void chatStream(List<ChatMessage> messages, StreamCallback callback);
}

The StreamCallback interface:

public interface StreamCallback {
    void onEvent(String content);
    void onComplete();
    void onError(Throwable t);
}

This simple interface allows for “drop-in” replacements of the backend model without changing the application logic.

ChatMessage

A simple POJO to represent a message in the conversation.

public class ChatMessage {
    private String role; // "user", "assistant", "system"
    private String content;
    // constructors, getters, setters
}

Implementations

Ollama (genai-ollama)

Connects to a local or remote Ollama instance.

  • Configuration: ollama.yml
    • ollamaUrl: URL of the Ollama server (e.g., http://localhost:11434).
    • model: The model name to use (e.g., llama3.1, mistral).
  • Protocol: Uses the /api/generate endpoint via HTTP/2.

AWS Bedrock (genai-bedrock)

Connects to Amazon Bedrock using the AWS SDK for Java v2.

  • Configuration: bedrock.yml
    • region: AWS Region (e.g., us-east-1).
    • modelId: The specific model ID (e.g., anthropic.claude-v2, amazon.titan-text-express-v1).
  • Authentication: Uses the standard AWS Default Credentials Provider Chain (Environment variables, Profile, IAM Roles).

OpenAI (genai-openai)

Connects to the OpenAI Chat Completions API.

  • Configuration: openai.yml
    • url: API endpoint (e.g., https://api.openai.com/v1/chat/completions).
    • model: The model to use (e.g., gpt-3.5-turbo, gpt-4).
    • apiKey: Your OpenAI API key.
  • Protocol: Uses standard HTTP/2 (or HTTP/1.1) to send JSON payloads.

Gemini (genai-gemini)

Connects to Google’s Gemini models (via Vertex AI or AI Studio).

  • Configuration: gemini.yml
    • url: API endpoint structure.
    • model: The model identifier (e.g., gemini-pro).
    • apiKey: Your Google API key.
  • Protocol: REST API with JSON payloads.

Code Example

The following example demonstrates how to use the interface to interact with a model, regardless of the underlying implementation.

// Logic to instantiate the correct client based on external configuration (e.g. from service.yml or reflection)
GenAiClient client;
if (useBedrock) {
    client = new BedrockClient();
} else if (useOpenAi) {
    client = new OpenAiClient();
} else if (useGemini) {
    client = new GeminiClient();
} else {
    client = new OllamaClient();
}

// Application logic remains agnostic
List<ChatMessage> messages = new ArrayList<>();
messages.add(new ChatMessage("user", "Explain quantum computing in 50 words."));
String response = client.chat(messages);
System.out.println(response);

// For subsequent turns:
messages.add(new ChatMessage("assistant", response));
messages.add(new ChatMessage("user", "What about entanglement?"));
String response2 = client.chat(messages);

Future Enhancements

  • Streaming Support: Add generateStream to support token streaming.
  • Chat Models: Add support for structured chat history (System, User, Assistant messages).
  • Tool Use: Support for function calling and tool use with models that support it.
  • More Providers: Integrations for OpenAI (ChatGPT), Google Vertex AI (Gemini), and others.

Technical Decisions

Use of Http2Client over JDK HttpClient

The implementation uses the light-4j Http2Client (wrapping Undertow) instead of the standard JDK HttpClient for the following reasons:

  1. Framework Consistency: Http2Client is the standard client within the light-4j ecosystem. Using it ensures consistent configuration, management, and behavior across all modules of the framework.
  2. Performance: It leverages the non-blocking I/O capabilities of the underlying Undertow server, sharing the same XNIO worker threads as the server components. This minimizes context switching and optimizes resource usage in a microservices environment.
  3. Callback Pattern: The ClientCallback and ChannelListener patterns are idiomatic to light-4j/Undertow. While they differ from the CompletableFuture style of the JDK client, using them maintains architectural uniformity for developers familiar with the framework’s internals.
  4. Integration: Utilizing the framework’s client allows for seamless integration with other light-4j features such as centralized SSL context management, connection pooling, and client-side observability.

For implementations that require vendor-specific logic (like AWS signing), we utilize the official vendor SDKs (e.g., AWS SDK for Java v2 for Bedrock) to handle complex authentication and protocol details efficiently.

Cross-Cutting-Concerns

Light-4j

Http Handler

Path Resource Handler

The PathResourceHandler is a middleware handler in light-4j that provides an easy way to serve static content from a specific filesystem path. It wraps Undertow’s PathHandler and ResourceHandler, allowing you to expose local directories via HTTP with simple configuration.

Features

  • External Configuration: Define path mapping and base directory in a YAML file.
  • Path Matching: Supports both exact path matching and prefix-based matching.
  • Directory Listing: Optional support for listing directory contents.
  • Performance: Integrated with Undertow’s PathResourceManager for efficient file serving.
  • Auto-Registration: Automatically registers with ModuleRegistry for runtime visibility.

Configuration (path-resource.yml)

The behavior of the handler is controlled by path-resource.yml.

# Path Resource Configuration
---
# The URL path at which the static content will be exposed (e.g., /static)
path: ${path-resource.path:/}

# The absolute filesystem path to the directory containing the static files
base: ${path-resource.base:/var/www/html}

# If true, the handler matches all requests starting with the 'path'.
# If false, it only matches exact hits on the 'path'.
prefix: ${path-resource.prefix:true}

# The minimum file size for transfer optimization (in bytes).
transferMinSize: ${path-resource.transferMinSize:1024}

# Whether to allow users to see a listing of files if they access a directory.
directoryListingEnabled: ${path-resource.directoryListingEnabled:false}

Setup

1. Add Dependency

Include the resource module in your pom.xml:

<dependency>
    <groupId>com.networknt</groupId>
    <artifactId>resource</artifactId>
    <version>${version.light-4j}</version>
</dependency>

2. Register Handler

In your handler.yml, register the PathResourceHandler and add it to your handler chain or path mappings.

handlers:
  - com.networknt.resource.PathResourceHandler@pathResource

chains:
  default:
    - ...
    - pathResource
    - ...

Or, if you want it on a specific path:

paths:
  - path: '/static'
    method: 'GET'
    exec:
      - pathResource

Operational Visibility

The path-resource module integrates with the ModuleRegistry. You can verify the active path and base directory mapping at runtime via the Server Info endpoint.

Virtual Host Handler

The VirtualHostHandler is a middleware handler in light-4j that enables name-based virtual hosting. It allows a single server instance to serve different content or applications based on the Host header in the incoming HTTP request. This is a wrapper around Undertow’s NameVirtualHostHandler.

Features

  • Domain-Based Routing: Route requests to different resource sets based on the domain name.
  • Flexible Mappings: Each virtual host can define its own path, base directory, and performance settings.
  • Centralized Configuration: All virtual hosts are defined in a single virtual-host.yml file.
  • Auto-Registration: Automatically registers with ModuleRegistry for administrative oversight.

Configuration (virtual-host.yml)

The configuration contains a list of host definitions.

# Virtual Host Configuration
---
hosts:
  - domain: dev.example.com
    path: /
    base: /var/www/dev
    transferMinSize: 1024
    directoryListingEnabled: true
  - domain: prod.example.com
    path: /app
    base: /var/www/prod/dist
    transferMinSize: 1024
    directoryListingEnabled: false

Host Parameters:

  • domain: The domain name to match (exact match against the Host header).
  • path: The URL prefix path within that domain to serve resources from.
  • base: The absolute filesystem path to the directory containing the static content.
  • transferMinSize: Minimum file size for transfer optimization (in bytes).
  • directoryListingEnabled: Whether to allow directory browsing for this specific host.

Setup

1. Add Dependency

Include the resource module in your pom.xml:

<dependency>
    <groupId>com.networknt</groupId>
    <artifactId>resource</artifactId>
    <version>${version.light-4j}</version>
</dependency>

2. Register Handler

In your handler.yml, register the VirtualHostHandler.

handlers:
  - com.networknt.resource.VirtualHostHandler@virtualHost

chains:
  default:
    - ...
    - virtualHost
    - ...

How it Works

When a request arrives, the VirtualHostHandler:

  1. Inspects the Host header of the request.
  2. Matches it against the domain entries in the configuration.
  3. If a match is found, it delegates the request to a subdomain-specific PathHandler and ResourceHandler configured with the corresponding base and path.
  4. If no match is found, the request typically proceeds down the chain or returns a 404 depending on the rest of the handler configuration.

Operational Visibility

The virtual-host module registers itself with the ModuleRegistry during initialization. The full list of configured virtual hosts and their mapping settings can be inspected via the Server Info endpoint.

Middleware Handler

Header Handler

The HeaderHandler is a middleware component in the light-4j framework designed to modify request and response headers as they pass through the handler chain. This is particularly useful for cross-cutting concerns such as:

  • Security: Updating or removing authorization headers (e.g., converting a Bearer token to a Basic Authorization header in a proxy scenario).
  • Privacy: Removing sensitive internal headers before sending a response to the client.
  • Context Propagation: Injecting correlation IDs or other context information into headers.
  • API Management: Standardizing headers across different microservices.

The handler is highly configurable, allowing for global header manipulation as well as path-specific configurations.

Configuration

The HeaderHandler uses a configuration file named header.yml to define its behavior.

Configuration Options

The configuration supports the following sections:

  1. enabled: A boolean flag to enable or disable the handler globally. Default is false.
  2. request: Defines header manipulations for incoming requests.
    • remove: A list of header names to remove from the request.
    • update: A map of key/value pairs to add or update in the request headers. If a key exists, its value is replaced.
  3. response: Defines header manipulations for outgoing responses.
    • remove: A list of header names to remove from the response.
    • update: A map of key/value pairs to add or update in the response headers.
  4. pathPrefixHeader: A map where keys are URL path prefixes and values are request and response configurations specific to that path. This allows granular control over header manipulation based on the endpoint being accessed.

Example header.yml (Fully Expanded)

# Enable header handler or not, default to false
enabled: false

# Global Request header manipulation
request:
  # Remove all the headers listed here
  remove:
  - header1
  - header2
  # Add or update the header with key/value pairs
  update:
    key1: value1
    key2: value2

# Global Response header manipulation
response:
  # Remove all the headers listed here
  remove:
  - header1
  - header2
  # Add or update the header with key/value pairs
  update:
    key1: value1
    key2: value2

# Per-path header manipulation
pathPrefixHeader:
  /petstore:
    request:
      remove:
        - headerA
        - headerB
      update:
        keyA: valueA
        keyB: valueB
    response:
      remove:
        - headerC
        - headerD
      update:
        keyC: valueC
        keyD: valueD
  /market:
    request:
      remove:
        - headerE
        - headerF
      update:
        keyE: valueE
        keyF: valueF
    response:
      remove:
        - headerG
        - headerH
      update:
        keyG: valueG
        keyH: valueH

Reference header.yml (Template)

This is the default configuration file packaged with the module (src/main/resources/config/header.yml). It uses placeholders that can be overridden by values.yml or handling external configurations.

# Enable header handler or not. The default to false and it can be enabled in the externalized
# values.yml file. It is mostly used in the http-sidecar, light-proxy or light-router.
enabled: ${header.enabled:false}
# Request header manipulation
request:
  # Remove all the request headers listed here. The value is a list of keys.
  remove: ${header.request.remove:}
  # Add or update the header with key/value pairs. The value is a map of key and value pairs.
  # Although HTTP header supports multiple values per key, it is not supported here.
  update: ${header.request.update:}
# Response header manipulation
response:
  # Remove all the response headers listed here. The value is a list of keys.
  remove: ${header.response.remove:}
  # Add or update the header with key/value pairs. The value is a map of key and value pairs.
  # Although HTTP header supports multiple values per key, it is not supported here.
  update: ${header.response.update:}
# requestPath specific header configuration. The entire object is a map with path prefix as the
# key and request/response like above as the value. For config format, please refer to test folder.
pathPrefixHeader: ${header.pathPrefixHeader:}

Configuring via values.yml

You can use values.yml to override specific properties. The properties can be defined in various formats (YAML, JSON string, comma-separated string) to suit different configuration sources (file system, config server).

# header.yml overrides
header.enabled: true

# List of strings (JSON format)
header.request.remove: ["header1", "header2"]

# Map (JSON format)
header.request.update: {"key1": "value1", "key2": "value2"}

# List (Comma separated string)
header.response.remove: header1,header2

# Map (Comma and colon separated string)
header.response.update: key1:value1,key2:value2

# Map (YAML format)
# Note: YAML format is suitable for file-based configuration. 
# For config server usage, use a JSON string representation.
header.pathPrefixHeader:
  /petstore:
    request:
      remove:
        - headerA
        - headerB
      update:
        keyA: valueA
        keyB: valueB
    response:
      remove:
        - headerC
        - headerD
      update:
        keyC: valueC
        keyD: valueD
  /market:
    request:
      remove:
        - headerE
        - headerF
      update:
        keyE: valueE
        keyF: valueF
    response:
      remove:
        - headerG
        - headerH
      update:
        keyG: valueG
        keyH: valueH

Usage

To use the HeaderHandler in your application:

  1. Add the Dependency: Ensure the header module is included in your project’s pom.xml.
  2. Register the Handler: Add com.networknt.header.HeaderHandler to your middleware chain in handler.yml.
  3. Configure: Provide a header.yml or configured values.yml in your src/main/resources/config folder (or external config directory) to define the desired header manipulations.

IP Whitelist

The ip-whitelist middleware handler is designed to secure specific endpoints (e.g., health checks, metric endpoints, admin screens) by allowing access only from trusted IP addresses. This is often used for endpoints that cannot be easily secured via OAuth 2.0 or other standard authentication mechanisms, or as an additional layer of security.

It serves as a critical component in scenarios where services like Consul, Prometheus, or internal orchestration tools need access to technical endpoints without user-level authentication.

Requirements

The handler supports:

  • Per-Path Configuration: Rules are defined for specific url path prefixes.
  • IPv4 and IPv6: Full support for both IP versions.
  • Flexible matching:
    • Exact match: e.g., 127.0.0.1 or FE45:00:00:000:0:AAA:FFFF:0045
    • Wildcard match: e.g., 10.10.*.* or FE45:00:00:000:0:AAA:FFFF:*
    • CIDR Notation (Slash): e.g., 127.0.0.48/30 or FE45:00:00:000:0:AAA:FFFF:01F4/127
  • Default Policy: Configurable default behavior (allow or deny) when no specific rule is matched.

Configuration

The configuration is managed via whitelist.yml.

Configuration Options

  • enabled: (boolean) Enable or disable the handler globally. Default true.
  • defaultAllow: (boolean) Determines the behavior for the IP addresses listed in the configuration.
    • true (Whitelist Mode - Most Common): If defaultAllow is true, the IPs listed for a path are ALLOWED. Any IP accessing that path that is not in the list is DENIED. If a path is not defined in the config, access is ALLOWED globally for that path.
    • false (Blacklist Mode): If defaultAllow is false, the IPs listed for a path are DENIED. Any IP accessing that path that is not in the list is ALLOWED. If a path is not defined in the config, access is DENIED globally for that path.
  • paths: A map where keys are request path prefixes and values are lists of IP patterns.

Example whitelist.yml (Template)

# IP Whitelist configuration

# Indicate if this handler is enabled or not.
enabled: ${whitelist.enabled:true}

# Default allowed or denied behavior.
defaultAllow: ${whitelist.defaultAllow:true}

# List of path prefixes and their access rules.
paths: ${whitelist.paths:}

Configuring via values.yml

You can define the rules in values.yml using either YAML or JSON format.

YAML Format

Suitable for file-based configuration.

whitelist.enabled: true
whitelist.defaultAllow: true
whitelist.paths:
  /health:
    - 127.0.0.1
    - 10.10.*.*
  /prometheus:
    - 192.168.1.5
    - 10.10.*.*
  /admin:
    - 127.0.0.1

JSON Format

Suitable for config servers or environment variables where a single string is required.

whitelist.paths: {"/health":["127.0.0.1","10.10.*.*"],"/prometheus":["192.168.1.5"]," /admin":["127.0.0.1"]}

Logic Flow

  1. Extract IP: The handler extracts the source IP address from the request.
  2. Match Path: It checks if the request path starts with any of the configured prefixes.
  3. Find ACL: If a matching path prefix is found, it retrieves the corresponding Access Control List containing rules (IP patterns).
  4. Evaluate Rules:
    • It iterates through the rules for the IP version (IPv4/IPv6).
    • If a rule matches the source IP:
      • It returns !rule.isDeny(). (In defaultAllow=true mode, rules are allow rules, so deny is false, returning true).
    • If no rule matches the source IP:
      • It returns !defaultAllow. (In defaultAllow=true mode, if IP is not in the allow list, it returns false (deny)).
  5. No Path Match: If the request path is not configured:
    • It returns defaultAllow. (In defaultAllow=true mode, unknown paths are open).

Error Handling

If access is denied, the handler terminates the exchange and returns an error.

  • Status Code: 403 Forbidden
  • Error Code: ERR10049
  • Message: INVALID_IP_FOR_PATH
  • Description: Peer IP %s is not in the whitelist for the endpoint %s

Usage

  1. Add the ip-whitelist module dependency to your pom.xml.
  2. Add com.networknt.whitelist.WhitelistHandler to your handler.yml chain.
  3. Configure whitelist.yml (or values.yml) with your specific IP rules.

Rate Limit

The rate-limit middleware handler is designed to protect your APIs from being overwhelmed by too many requests. It can be used for two primary purposes:

  1. DDoS Protection: Limiting concurrent requests from the public internet to prevent denial-of-service attacks.
  2. Throttling: Protecting slow backend services or managing API quotas for different clients or users.

This handler impacts performance slightly, so it is typically not enabled by default. It should be placed early in the middleware chain, typically after ExceptionHandler and MetricsHandler, to fail fast and still allow metrics to capture rate-limiting events.

Dependency

To use the handler, add the following dependency to your pom.xml:

<dependency>
    <groupId>com.networknt</groupId>
    <artifactId>rate-limit</artifactId>
    <version>${version.light-4j}</version>
</dependency>

Note: The configuration class is located in the limit-config module.

Handler Configuration

Register the handler in handler.yml:

handlers:
  - com.networknt.limit.LimitHandler@limit

chains:
  default:
    - exception
    - metrics
    - limit
    - ...

Configuration (limit.yml)

The handler is configured via limit.yml.

# Rate Limit Handler Configuration

# Enable or disable the handler
enabled: ${limit.enabled:false}

# Error code returned when limit is reached. 
# Use 503 for DDoS protection (to trick attackers) or 429 for internal throttling.
errorCode: ${limit.errorCode:429}

# Default rate limit: e.g., 10 requests per second and 10000 quota per day.
rateLimit: ${limit.rateLimit:10/s 10000/d}

# If true, rate limit headers are always returned, even for successful requests.
headersAlwaysSet: ${limit.headersAlwaysSet:false}

# Key of the rate limit: server, address, client, user
# server: Shared limit for the entire server.
# address: Limit per IP address.
# client: Limit per client ID (from JWT).
# user: Limit per user ID (from JWT).
key: ${limit.key:server}

# Custom limits can be defined for specific keys
server: ${limit.server:}
address: ${limit.address:}
client: ${limit.client:}
user: ${limit.user:}

# Key Resolvers
clientIdKeyResolver: ${limit.clientIdKeyResolver:com.networknt.limit.key.JwtClientIdKeyResolver}
addressKeyResolver: ${limit.addressKeyResolver:com.networknt.limit.key.RemoteAddressKeyResolver}
userIdKeyResolver: ${limit.userIdKeyResolver:com.networknt.limit.key.JwtUserIdKeyResolver}

Rate Limit Keys

1. Server Key

The limit is shared across all incoming requests. You can define specific limits for different path prefixes:

limit.server:
  /v1/address: 10/s
  /v2/address: 1000/s

2. Address Key

The source IP address is used as the key. Each IP gets its own quota.

limit.address:
  192.168.1.100: 10/h 1000/d
  192.168.1.102:
    /v1: 10/s

3. Client Key

The client_id from the JWT token is used as the key. Note: JwtVerifierHandler must be in the chain before the limit handler.

limit.client:
  my-client-id: 100/m 10000/d

4. User Key

The user_id from the JWT token is used as the key. Note: JwtVerifierHandler must be in the chain before the limit handler.

limit.user:
  [email protected]: 10/m 10000/d

Rate Limit Headers

When a limit is reached (or if headersAlwaysSet is true), the following headers are returned:

  • X-RateLimit-Limit: The configured limit (e.g., 10/s).
  • X-RateLimit-Remaining: The number of requests remaining in the current time window.
  • X-RateLimit-Reset: The number of seconds until the current window resets.
  • Retry-After: (Only when limit is reached) The timestamp when the client can retry.

Module Registration

The limit-config module automatically registers itself with the ModuleRegistry when LimitConfig.load() is called. This allows the configuration to be visible via the Server Info endpoint and supports hot reloading of configurations.

Prometheus Metrics

The prometheus module in light-4j provides a systems monitoring middleware handler that integrates with Prometheus. Unlike other metrics modules that “push” data to a central server (e.g., InfluxDB), Prometheus adopts a pull-based model where the Prometheus server periodically scrapes metrics from instrumented targets.

Features

  • Runtime Collection: Captures HTTP request counts and response times using the Prometheus dimensional data model.
  • Dimensionality: Automatically attaches labels like endpoint and clientId to every metric, allowing for granular filtering and aggregation in Grafana.
  • JVM Hotspot Monitoring: Optional collection of JVM-level metrics including CPU usage, memory pools, garage collection, and thread states.
  • Standardized Scrape Endpoint: Provides a dedicated handler to expose metrics in the standard Prometheus text format.
  • Auto-Registration: The module automatically registers its configuration with the ModuleRegistry during initialization.

Configuration (prometheus.yml)

The behavior of the Prometheus middleware is controlled by the prometheus.yml file.

# Prometheus Metrics Configuration
---
# If metrics handler is enabled or not. Default is false.
enabled: ${prometheus.enabled:false}

# If the Prometheus hotspot monitor is enabled or not. 
# includes thread, memory, classloader statistics, etc.
enableHotspot: ${prometheus.enableHotspot:false}

Setup

To enable Prometheus metrics, you need to add two handlers to your handler.yml: one to collect the data and another to expose it as an endpoint for scraping.

1. Add Handlers to handler.yml

handlers:
  # Captures metrics for every request
  - com.networknt.metrics.prometheus.PrometheusHandler@prometheus
  # Exposes the metrics to the Prometheus server
  - com.networknt.metrics.prometheus.PrometheusGetHandler@getprometheus

chains:
  default:
    - exception
    - prometheus  # Place early in the chain
    - correlation
    - ...

2. Define the Scrape Path

You must expose a GET endpoint (typically /v1/prometheus or /metrics) that invoke the getprometheus handler.

paths:
  - path: '/v1/prometheus'
    method: 'get'
    exec:
      - getprometheus

Collected Metrics

The PrometheusHandler defines several core application metrics:

  • requests_total: Total number of HTTP requests received.
  • success_total: Total number of successful requests (status codes 200-399).
  • auth_error_total: Total number of authentication/authorization errors (status codes 401, 403).
  • request_error_total: Total number of client-side request errors (status codes 400-499).
  • server_error_total: Total number of server-side errors (status codes 500+).
  • response_time_seconds: A summary of HTTP response latencies.

JVM Hotspot Monitoring

When enableHotspot is set to true, the module initializes the Prometheus DefaultExports, which captures:

  • Process Statistics: process_cpu_seconds_total, process_open_fds.
  • Memory Usage: jvm_memory_pool_bytes_used, jvm_memory_pool_bytes_max.
  • Thread Areas: jvm_threads_state, jvm_threads_deadlocked.
  • Garbage Collection: jvm_gc_collection_seconds.

Scraping with Prometheus

To pull data from your service, configure your Prometheus server’s scrape_configs in its configuration file:

scrape_configs:
  - job_name: 'light-4j-services'
    scrape_interval: 15s
    metrics_path: /v1/prometheus
    static_configs:
      - targets: ['localhost:8080']
    scheme: https
    tls_config:
      insecure_skip_verify: true

Dependency

Add the following to your pom.xml:

<dependency>
    <groupId>com.networknt</groupId>
    <artifactId>prometheus</artifactId>
    <version>${version.light-4j}</version>
</dependency>

Module Registration

The prometheus module registers itself with the ModuleRegistry automatically when PrometheusConfig.load() is called. This registration (along with the current configuration values) can be inspected via the Server Info endpoint.

Sanitizer Handler

The SanitizerHandler is a middleware component in light-4j designed to address Cross-Site Scripting (XSS) concerns by sanitizing request headers and bodies. It leverages the OWASP Java Encoder to perform context-aware encoding of potentially malicious user input.

Features

  • Context-Aware Encoding: Uses the owasp-java-encoder to safely encode data.
  • Header Sanitization: Encodes request headers to prevent header-based XSS or injection.
  • Body Sanitization: Deep-scans JSON request bodies and encodes string values in Maps and Lists.
  • Selective Sanitization: Fine-grained control with AttributesToEncode and AttributesToIgnore lists for both headers and bodies.
  • Flexible Encoders: Supports multiple encoding formats like javascript-source, javascript-attribute, javascript-block, etc.
  • Auto-Registration: Automatically registers with ModuleRegistry during configuration loading for runtime visibility.

Dependency

To use the SanitizerHandler, include the following dependency in your pom.xml:

<dependency>
    <groupId>com.networknt</groupId>
    <artifactId>sanitizer</artifactId>
    <version>${version.light-4j}</version>
</dependency>

Configuration (sanitizer.yml)

The behavior of the handler is managed through sanitizer.yml.

# Sanitizer Configuration
---
# Indicate if sanitizer is enabled or not. Default is false.
enabled: ${sanitizer.enabled:false}

# --- Body Sanitization ---
# If enabled, the request body will be sanitized. 
# Note: Requires BodyHandler to be in the chain before SanitizerHandler.
bodyEnabled: ${sanitizer.bodyEnabled:true}

# The encoder for the body. Default is javascript-source.
bodyEncoder: ${sanitizer.bodyEncoder:javascript-source}

# Optional list of body keys to encode. If specified, ONLY these keys are scanned.
bodyAttributesToEncode: ${sanitizer.bodyAttributesToEncode:}

# Optional list of body keys to ignore. All keys EXCEPT these will be scanned.
bodyAttributesToIgnore: ${sanitizer.bodyAttributesToIgnore:}

# --- Header Sanitization ---
# If enabled, the request headers will be sanitized.
headerEnabled: ${sanitizer.headerEnabled:true}

# The encoder for the headers. Default is javascript-source.
headerEncoder: ${sanitizer.headerEncoder:javascript-source}

# Optional list of header keys to encode. If specified, ONLY these headers are scanned.
headerAttributesToEncode: ${sanitizer.headerAttributesToEncode:}

# Optional list of header keys to ignore. All headers EXCEPT these will be scanned.
headerAttributesToIgnore: ${sanitizer.headerAttributesToIgnore:}

Setup

1. Order in Chain

If bodyEnabled is set to true, the SanitizerHandler must be placed in the handler chain after the BodyHandler. This is because the sanitizer expects the request body to be already parsed into the exchange attachment.

2. Register Handler

In your handler.yml, register the handler and add it to your chain.

handlers:
  - com.networknt.sanitizer.SanitizerHandler@sanitizer

chains:
  default:
    - ...
    - body
    - sanitizer
    - ...

When to use Sanitizer

The SanitizerHandler is primarily intended for applications that collect user input via Web or Mobile UIs and later use that data to generate HTML pages (e.g., forums, blogs, profile pages).

Do not use this handler for:

  • Services where input is only processed internally and never rendered back to a browser.
  • Encrypted or binary data.
  • High-performance logging services where the overhead of string manipulation is unacceptable.

Query Parameters

The SanitizerHandler does not process query parameters. Modern web servers like Undertow already perform robust sanitization and decoding of special characters in query parameters before they reach the handler chain.

Operational Visibility

The sanitizer module utilizes the Singleton pattern for its configuration. During startup, it automatically registers itself with the ModuleRegistry. You can verify the current configuration, including active encoders and ignore/encode lists, at runtime via the Server Info endpoint.

Encode Library

The underlying library used for sanitization is the OWASP Java Encoder. The default encoding level is javascript-source, which provides a high degree of security without corrupting most types of textual data.

Security Handler

The SecurityHandler (implemented as JwtVerifyHandler in REST, GraphQL, etc.) is responsible for verifying the security tokens (JWT or SWT) in the request header. It is a core component of the light-4j framework and provides distributed policy enforcement without relying on a centralized gateway.

Configuration

The security configuration is usually defined in security.yml, but it can be overridden by framework-specific files like openapi-security.yml or graphql-security.yml.

Example security.yml

# Security configuration for security module in light-4j.
---
# Enable JWT verification flag.
enableVerifyJwt: ${security.enableVerifyJwt:true}

# Enable SWT verification flag.
enableVerifySwt: ${security.enableVerifySwt:true}

# swt clientId header name.
swtClientIdHeader: ${security.swtClientIdHeader:swt-client}

# swt clientSecret header name. 
swtClientSecretHeader: ${security.swtClientSecretHeader:swt-secret}

# Extract JWT scope token from the X-Scope-Token header and validate the JWT token
enableExtractScopeToken: ${security.enableExtractScopeToken:true}

# Enable JWT scope verification. Only valid when enableVerifyJwt is true.
enableVerifyScope: ${security.enableVerifyScope:true}

# Skip scope verification if the endpoint specification is missing.
skipVerifyScopeWithoutSpec: ${security.skipVerifyScopeWithoutSpec:false}

# If set true, the JWT verifier handler will pass if the JWT token is expired already.
ignoreJwtExpiry: ${security.ignoreJwtExpiry:false}

# User for test only. should be always be false on official environment.
enableMockJwt: ${security.enableMockJwt:false}

# Enables relaxed verification for jwt. e.g. Disables key length requirements.
enableRelaxedKeyValidation: ${security.enableRelaxedKeyValidation:false}

# JWT signature public certificates. kid and certificate path mappings.
jwt:
  certificate: ${security.certificate:100=primary.crt&101=secondary.crt}
  clockSkewInSeconds: ${security.clockSkewInSeconds:60}
  # Key distribution server standard: JsonWebKeySet for other OAuth 2.0 provider| X509Certificate for light-oauth2
  keyResolver: ${security.keyResolver:JsonWebKeySet}

# Enable or disable JWT token logging for audit.
logJwtToken: ${security.logJwtToken:true}

# Enable or disable client_id, user_id and scope logging.
logClientUserScope: ${security.logClientUserScope:false}

# Enable JWT token cache to speed up verification.
enableJwtCache: ${security.enableJwtCache:true}

# Max size of the JWT cache before a warning is logged.
jwtCacheFullSize: ${security.jwtCacheFullSize:100}

# Retrieve public keys dynamically from the OAuth2 provider.
bootstrapFromKeyService: ${security.bootstrapFromKeyService:false}

# Provider ID for federated deployment.
providerId: ${security.providerId:}

# Define a list of path prefixes to skip security.
skipPathPrefixes: ${security.skipPathPrefixes:}

# Pass specific claims from the token to the backend API via HTTP headers.
passThroughClaims:
  clientId: client_id
  tokenType: token_type

Key Features

JWT Verification

Verification includes signature check, expiration check, and scope verification. The handler uses the JwtVerifier to perform these checks.

SWT Verification

The handler also supports Shared Secret Token (SWT) verification. When enabled, it checks the token using client ID and secret, which can be passed in headers specified by swtClientIdHeader and swtClientSecretHeader.

Scope Verification

If enableVerifyScope is true, the handler compares the scopes in the JWT against the scopes defined in the API specification (e.g., openapi.yaml). If the specification is missing and skipVerifyScopeWithoutSpec is true, scope verification is skipped.

Token Caching

To improve performance, verified tokens can be cached. This avoids expensive signature verification for subsequent requests with the same token. The cache size can be monitored using jwtCacheFullSize.

Dynamic Key Loading

By setting bootstrapFromKeyService to true, the handler can pull public keys (JWK) from the OAuth2 provider’s key service based on the kid (Key ID) in the JWT header.

Path Skipping

You can bypass security for specific endpoints by adding their prefixes to skipPathPrefixes.

Claim Pass-through

Claims from the JWT or SWT can be mapped to HTTP headers and passed to the backend service using passThroughClaims. This is useful for passing user information or client details without the backend having to parse the token again.

Security Attacks Mitigation

alg header attacks

The light-4j implementation is opinionated and primarily uses RS256. It prevents “alg: none” attacks and HMAC-RSA confusion attacks by explicitly specifying the expected algorithm.

kid and Key Rotation

The use of kid allows the framework to support multiple keys simultaneously, which is essential for seamless key rotation.

Unified Security

The UnifiedSecurityHandler is a powerful middleware that consolidates various security mechanisms into a single handler. It is designed to simplify security configuration, especially in complex environments like a shared light-gateway, where different paths might require different authentication methods.

By using the UnifiedSecurityHandler, you avoid chaining multiple security-specific handlers (like BasicAuthHandler, JwtVerifyHandler, ApiKeyHandler) and can manage all security policies in one place.

Configuration

The configuration for this handler is defined in unified-security.yml.

Example unified-security.yml

# Unified security configuration.
---
# Indicate if this handler is enabled.
enabled: ${unified-security.enabled:true}

# Anonymous prefixes configuration. Request paths starting with these prefixes bypass all security.
anonymousPrefixes: ${unified-security.anonymousPrefixes:}

# Path prefix security configuration.
pathPrefixAuths:
  - prefix: /api/v1
    basic: false
    jwt: true
    sjwt: false
    swt: false
    apikey: false
    jwkServiceIds: com.networknt.petstore-1.0.0
  - prefix: /api/v2
    basic: true
    jwt: true
    apikey: true
    jwkServiceIds: service1,service2

Configuration Parameters

ParameterDescription
enabledWhether the UnifiedSecurityHandler is active.
anonymousPrefixesA list of path prefixes that do not require any authentication.
pathPrefixAuthsA list of configurations defining security policies for specific path prefixes.

Path Prefix Auth Fields

FieldDescription
prefixThe request path prefix this policy applies to.
basicEnable Basic Authentication verification.
jwtEnable standard JWT verification (with scope check).
sjwtEnable Simple JWT verification (signature check only, no scope check).
swtEnable Shared Secret Token (SWT) verification.
apikeyEnable API Key verification.
jwkServiceIdsComma-separated list of service IDs for JWK lookup (used if jwt is true).
sjwkServiceIdsComma-separated list of service IDs for JWK lookup (used if sjwt is true).
swtServiceIdsComma-separated list of service IDs for introspection servers (used if swt is true).

How it works

  1. Anonymous Check: The handler first checks if the request path matches any prefix in anonymousPrefixes. If it does, the request proceeds to the next handler immediately.
  2. Path Policy Lookup: The handler iterates through pathPrefixAuths to find a match for the request path.
  3. Authentication Execution:
    • If Basic, JWT, SJWT, or SWT is enabled, the handler looks for the Authorization header.
    • It identifies the authentication type (Basic vs. Bearer).
    • For Bearer tokens, it determines if the token is a JWT/SJWT or an SWT.
    • If SJWT (Simple JWT) is enabled, it can differentiate between a full JWT (with scopes) and a Simple JWT (without scopes) and invoke the appropriate verification logic.
    • If multiple methods are enabled (e.g., both JWT and Basic), it attempts to verify based on the provided credentials.
    • If ApiKey is enabled, it checks for the API key in the configured headers/parameters.

Benefits

  • Centralized Management: Configure all security policies for the entire gateway or service in one file.
  • Reduced Performance Overhead: Minimizes the number of handlers in the middleware chain.
  • Flexibility: Supports different security requirements for different API versions or functional areas.
  • Dynamic Discovery: Supports multiple JWK or introspection services per path prefix, enabling integration with multiple identity providers.

Sidecar Handler

The sidecar module provides a set of middleware handlers designed to operate in a sidecar or gateway environment. It enables the separation of cross-cutting concerns from business logic, allowing the light-sidecar to act as a micro-gateway for both incoming (ingress) and outgoing (egress) traffic.

Overview

In a sidecar deployment (typically within a Kubernetes Pod), the sidecar module manages traffic between the backend service and the external network. It allows the backend service—regardless of the language or framework it’s built with—to benefit from light-4j features like security, discovery, and observability.

Configuration (sidecar.yml)

The behavior of the sidecar handlers is controlled via sidecar.yml. This configuration is mapped to the SidecarConfig class.

Example sidecar.yml

# Light http sidecar configuration
---
# Indicator used to determine the condition for router traffic.
# Valid values: 'header' (default), 'protocol', or other custom indicators.
# If 'header', it looks for service_id or service_url in the request headers.
# If 'protocol', it treats HTTP traffic as egress and HTTPS as ingress (or vice versa depending on setup).
egressIngressIndicator: ${sidecar.egressIngressIndicator:header}

Parameters

ParameterDefaultDescription
egressIngressIndicatorheaderDetermines if a request is Ingress or Egress.

Ingress vs. Egress Logic

The SidecarRouterHandler and related middleware use the egressIngressIndicator to decide how to process a request:

  1. Ingress (Incoming): Traffic coming from external clients to the backend API. The sidecar applies security (Token validation), logging, and then proxies the request to the backend API.
  2. Egress (Outgoing): Traffic initiated by the backend API to call another service. The sidecar handles service discovery, load balancing, and token injection (via SAML or OAuth2) before forwarding the request to the target service.

Sidecar Middleware Handlers

The sidecar module includes specialized versions of standard middleware that are aware of the traffic direction.

SidecarTokenHandler

Validates tokens for Ingress traffic. If the request is identified as Egress (based on the indicator), it skips validation to allow the request to proceed to the external service.

SidecarSAMLTokenHandler

Similar to the Token Handler, it handles SAML token validation for Ingress traffic and skips for Egress.

SidecarServiceDictHandler

Applies service dictionary mapping for Egress traffic, ensuring the outgoing request is correctly mapped to a target service definition.

SidecarPathPrefixServiceHandler

Identifies the target service ID based on the path prefix for Egress traffic. For Ingress traffic, it populates audit attachments and passes the request to the proxy.

Implementation Details

The sidecar module follows the standard light-4j implementation patterns:

  • Singleton + Caching: SidecarConfig uses a cached singleton instance for performance, loaded via SidecarConfig.load().
  • Hot Reload: Supports runtime configuration updates via the reload() method, which re-registers the module in the ModuleRegistry.
  • Module Registry Integration: Automatically registers itself with /server/info to provide visibility into the active sidecar configuration.

SSE Handler

The SSE (Server-Sent Events) Handler in light-4j allows the server to push real-time updates to the client over a standard HTTP connection. This handler manages the connection lifecycle and keep-alive messages.

Configuration

The configuration for the SSE Handler is defined in sse.yml (or sse.json/yaml). It corresponds to the SseConfig.java class.

Configuration Properties

PropertyTypeDefaultDescription
enabledbooleantrueEnable or disable the SSE Handler.
pathstring/sseThe default endpoint path for the SSE Handler.
keepAliveIntervalint10000The keep-alive interval in milliseconds. Used for the default path or if no specific interval is defined for a prefix.
pathPrefixeslistnullA list of path prefixes with specific keep-alive intervals. See example below.
metricsInjectionbooleanfalseIf true, injects metrics for the downstream API response time (useful in sidecar/gateway modes).
metricsNamestringrouter-responseThe name used for metrics categorization if injection is enabled.

Configuration Example (sse.yml)

# Enable SSE Handler
enabled: true

# Default SSE Endpoint Path
path: /sse

# Default keep-alive interval in milliseconds
keepAliveInterval: 10000

# Define path prefix related configuration properties as a list of key/value pairs.
# If request path cannot match to one of the pathPrefixes or the default path, the request will be skipped.
pathPrefixes:
  - pathPrefix: /sse/abc
    keepAliveInterval: 20000
  - pathPrefix: /sse/def
    keepAliveInterval: 40000

# Metrics injection (optional, for gateway/sidecar usage)
metricsInjection: false
metricsName: router-response

Usage

Register the SseHandler in your handler.yml chain. When a client connects to the configured path (e.g., /sse), the handler will establish a persistent connection and manage keep-alive signals based on the keepAliveInterval.

Path Matching

  1. Prefix Matching: If pathPrefixes are configured, the handler checks if the request path starts with any of the defined prefixes. If a match is found, the specific keepAliveInterval for that prefix is used.
  2. Exact Matching: If no prefix matches, the handler checks if the request path exactly matches the default path (/sse).
  3. Passthrough: If neither matches, the request is passed to the next handler in the chain.

Traceability

The handler supports traceability via the X-Traceability-Id header or an id query parameter to track connections in the SseConnectionRegistry.

MCP Router

The MCP (Model Context Protocol) Router in light-4j allows binding LLM tools to HTTP endpoints, enabling an AI model to interact with your services via the MCP standard. It supports both Server-Sent Events (SSE) for connection establishment and HTTP POST for JSON-RPC messages.

Configuration

The configuration for the MCP Router is defined in mcp-router.yml (or mcp-router.json/yaml). It corresponds to the McpConfig.java class.

Configuration Properties

PropertyTypeDefaultDescription
enabledbooleantrueEnable or disable the MCP Router Handler.
ssePathstring/mcp/sseThe endpoint path for establishing the MCP Server-Sent Events (SSE) connection.
messagePathstring/mcp/messageThe endpoint path for sending MCP JSON-RPC messages (POST).
toolslistnullA list of tools exposed by this router. Each tool maps to a downstream HTTP service.

Configuration Example (mcp-router.yml)

# Enable MCP Router Handler
enabled: true

# Path for MCP Server-Sent Events (SSE) endpoint
ssePath: /mcp/sse

# Path for MCP JSON-RPC message endpoint
messagePath: /mcp/message

# Define tools exposed by this router
tools:
  - name: getWeather
    description: Get the weather for a location
    host: https://api.weather.com
    path: /v1/current
    method: GET
    # inputSchema is optional; default is type: object

Architecture

The MCP Router implements the MCP HTTP transport specification:

  1. Connection: Clients connect to the ssePath (default /mcp/sse) via GET to establish an SSE session. The server responds with an endpoint event containing the URI for message submission.
  2. Messaging: Clients send JSON-RPC 2.0 messages to the messagePath (default /mcp/message) via POST.
  3. Tool Execution: The router translates tools/call requests into HTTP requests to the configured downstream services (host + path).

Supported Methods

The router supports the following MCP JSON-RPC methods:

  • initialize: Returns server capabilities (tools list changed notification) and server info.
  • notifications/initialized: Acknowledgment from the client.
  • tools/list: Lists all configured tools with their names, descriptions, and input schemas.
  • tools/call: Executes a specific tool by name. The arguments in the request are passed to the downstream service.

Usage

Register the McpHandler in your handler.yml chain. Ensure the ssePath and messagePath are accessible.

Example

paths:
  - path: '/mcp/sse'
    method: 'GET'
    exec:
      - mcp-router
  - path: '/mcp/message'
    method: 'POST'
    exec:
      - mcp-router

Interceptor

Request Transformer Interceptor

The request-transformer is a powerful middleware interceptor that allows for dynamic modification of incoming requests using the light-4j rule engine. It provides a highly flexible way to manipulate request metadata (headers, query parameters, path) and the request body based on custom business logic defined in rules.

Features

  • Dynamic Transformation: Modify request elements at runtime without changing application code.
  • Rule-Based: Leverage the light-4j rule engine to define complex transformation logic.
  • Path-Based Activation: Target specific request paths using the appliedPathPrefixes configuration.
  • Metadata Manipulation: Add, update, or remove request headers, query parameters, path, and URI.
  • Body Transformation: Overwrite the request body (supports JSON, XML, and other text-based formats).
  • Short-Circuiting: Generate an immediate response body or validation error to return to the caller, stopping the handler chain.
  • Encoding Support: Configurable body encoding per path prefix for legacy API compatibility.
  • Auto-Registration: Automatically registers with the ModuleRegistry for administrative visibility.

Configuration (request-transformer.yml)

The interceptor is configured via request-transformer.yml.

# Request Transformer Configuration
---
# Indicate if this interceptor is enabled or not. Default is true.
enabled: ${request-transformer.enabled:true}

# Indicate if the transform interceptor needs to change the request body. Default is true.
requiredContent: ${request-transformer.requiredContent:true}

# Default body encoding for the request body. Default is UTF-8.
defaultBodyEncoding: ${request-transformer.defaultBodyEncoding:UTF-8}

# A list of applied request path prefixes. Only requests matching these prefixes will be processed.
# This can be a single string or a list of strings.
appliedPathPrefixes: ${request-transformer.appliedPathPrefixes:}

# Customized encoding for specific path prefixes.
# This is useful for legacy APIs that require non-UTF-8 encoding (e.g., ISO-8859-1).
# pathPrefixEncoding:
#   /v1/pets: ISO-8859-1
#   /v1/party/info: ISO-8859-1

Setup

1. Add Dependency

Add the following dependency to your pom.xml:

<dependency>
    <groupId>com.networknt</groupId>
    <artifactId>request-transformer</artifactId>
    <version>${version.light-4j}</version>
</dependency>

2. Register Interceptor

In your handler.yml, add the RequestTransformerInterceptor to the interceptors list and include it in the appropriate handler chains.

handlers:
  - com.networknt.reqtrans.RequestTransformerInterceptor@requestTransformer

chains:
  default:
    - ...
    - requestTransformer
    - ...

Transformation Rules

The interceptor passes a context map (objMap) to the rule engine. Your rules can inspect these values and return a result map to trigger specific modifications.

Rule Input Map (objMap)

  • auditInfo: Map of audit information (e.g., clientId, endpoint).
  • requestHeaders: Map of current request headers.
  • queryParameters: Map of current query parameters.
  • pathParameters: Map of current path parameters.
  • method: HTTP method (e.g., “POST”, “GET”).
  • requestURL: The full request URL.
  • requestURI: The request URI.
  • requestPath: The request path.
  • requestBody: The request body string (only if requiredContent is true).

Rule Output Result Map

To perform a transformation, your rule must return a map containing one or more of the following keys:

  • requestPath: String - Overwrites the exchange request path.
  • requestURI: String - Overwrites the exchange request URI.
  • queryString: String - Overwrites the exchange query string.
  • requestHeaders: Map - Contains two sub-keys:
    • remove: List<String> - List of header names to remove.
    • update: Map<String, String> - Map of header names and values to add or update.
  • requestBody: String - Overwrites the request body buffer with the provided string.
  • responseBody: String - Immediately returns this string as the response body, bypassing the rest of the chain.
    • Also supports statusCode (Integer) and contentType (String) in the result map.
  • validationError: Map - Short-circuits the request with a validation error.
    • Requires errorMessage (String), contentType (String), and statusCode (Integer).

Example Transformation Logic

If a rule decides to update a header and the request path, the result map returned from the rule engine might look like this:

{
  "result": true,
  "requestPath": "/v2/new-endpoint",
  "requestHeaders": {
    "update": {
      "X-Transformation-Status": "transformed"
    },
    "remove": ["X-Old-Header"]
  }
}

Operational Visibility

The request-transformer module automatically registers itself with the ModuleRegistry during configuration loading. You can inspect its current status and configuration parameters at runtime via the Server Info endpoint.

Response Transformer Interceptor

The response-transformer is a powerful middleware interceptor that allows for dynamic modification of outgoing responses using the light-4j rule engine. It provides a highly flexible way to manipulate response headers and the response body based on custom business logic defined in rules.

Features

  • Dynamic Transformation: Modify response elements at runtime without changing application code.
  • Rule-Based: Leverage the light-4j rule engine to define complex transformation logic.
  • Path-Based Activation: Target specific request paths using the appliedPathPrefixes configuration.
  • Header Manipulation: Add, update, or remove response headers.
  • Body Transformation: Overwrite the response body (supports JSON, XML, and other text-based formats).
  • Encoding Support: Configurable body encoding per path prefix for legacy API compatibility.
  • Auto-Registration: Automatically registers with the ModuleRegistry during configuration loading for administrative visibility.

Configuration (response-transformer.yml)

The interceptor is configured via response-transformer.yml.

# Response Transformer Configuration
---
# Indicate if this interceptor is enabled or not. Default is true.
enabled: ${response-transformer.enabled:true}

# Indicate if the transform interceptor needs to change the response body. Default is true.
requiredContent: ${response-transformer.requiredContent:true}

# Default body encoding for the response body. Default is UTF-8.
defaultBodyEncoding: ${response-transformer.defaultBodyEncoding:UTF-8}

# A list of applied request path prefixes. Only requests matching these prefixes will be processed.
appliedPathPrefixes: ${response-transformer.appliedPathPrefixes:}

# For certain path prefixes that are not using the defaultBodyEncoding UTF-8, you can define the customized
# encoding like ISO-8859-1 for the path prefixes here.
# pathPrefixEncoding:
#   /v1/pets: ISO-8859-1
#   /v1/party/info: ISO-8859-1

Setup

1. Add Dependency

Include the following dependency in your pom.xml:

<dependency>
    <groupId>com.networknt</groupId>
    <artifactId>response-transformer</artifactId>
    <version>${version.light-4j}</version>
</dependency>

2. Register Interceptor

In your handler.yml, add the ResponseTransformerInterceptor to the interceptors list and include it in the appropriate handler chains.

handlers:
  - com.networknt.restrans.ResponseTransformerInterceptor@responseTransformer

chains:
  default:
    - ...
    - responseTransformer
    - ...

Transformation Rules

The interceptor passes a context map (objMap) to the rule engine. Your rules can inspect these values and return a result map to trigger specific modifications.

Rule Input Map (objMap)

  • auditInfo: Map of audit information (e.g., clientId, endpoint).
  • requestHeaders: Map of current request headers.
  • responseHeaders: Map of current response headers.
  • queryParameters: Map of current query parameters.
  • pathParameters: Map of current path parameters.
  • method: HTTP method (e.g., “POST”, “GET”).
  • requestURL: The full request URL.
  • requestURI: The request URI.
  • requestPath: The request path.
  • requestBody: The request body (if available in the exchange attachment).
  • responseBody: The original response body string (only if requiredContent is true).
  • statusCode: The HTTP status code of the response.

Rule Output Result Map

To perform a transformation, your rule must return a map containing one or more of the following keys:

  • responseHeaders: Map - Contains two sub-keys:
    • remove: List<String> - List of header names to remove.
    • update: Map<String, String> - Map of header names and values to add or update.
  • responseBody: String - Overwrites the response body buffer with the provided string.

Example Transformation Logic

If a rule decides to update a header and the response body, the result map returned from the rule engine might look like this:

{
  "result": true,
  "responseHeaders": {
    "update": {
      "X-Transformation-Version": "2.0"
    },
    "remove": ["Server"]
  },
  "responseBody": "{\"status\":\"success\", \"data\": \"transformed content\"}"
}

Operational Visibility

The response-transformer module automatically registers itself with the ModuleRegistry. This allows administrators to inspect the active configuration and status of the interceptor at runtime via the Server Info endpoint.

Admin Endpoint

Server Info

Introduction

The ServerInfoGetHandler is a middleware component in the light-4j framework that provides a comprehensive overview of the server’s runtime state. This includes:

  • Environment Info: Host IP, hostname, DNS, and runtime metrics (processors, memory).
  • System Properties: Java version, OS details, timezone.
  • Component Configuration: Configuration details for all registered modules.
  • Specification: The API specification (OpenAPI, etc.) if applicable.

This handler is crucial for monitoring, debugging, and integration with the light-controller and light-portal for runtime dashboards.

Configuration

The handler is configured via info.yml.

# Server info endpoint that can output environment and component along with configuration

# Indicate if the server info is enabled or not.
enableServerInfo: ${info.enableServerInfo:true}

# String list keys that should not be sorted in the normalized info output.
keysToNotSort: ${info.keysToNotSort:["admin", "default", "defaultHandlers", "request", "response"]}

# Downstream configuration (for gateways/sidecars)
# Indicate if the server info needs to invoke downstream APIs.
downstreamEnabled: ${info.downstreamEnabled:false}
# Downstream API host.
downstreamHost: ${info.downstreamHost:http://localhost:8081}
# Downstream API server info path.
downstreamPath: ${info.downstreamPath:/adm/server/info}

If enableServerInfo is false, the endpoint will return an error ERR10013 - SERVER_INFO_DISABLED.

Handler Registration

Unlike most middleware handlers, ServerInfoGetHandler is typically configured as a normal handler in handler.yml but mapped to a specific path like /server/info.

handlers:
  - com.networknt.info.ServerInfoGetHandler@info

paths:
  - path: '/server/info'
    method: 'get'
    exec:
      - security
      - info

Security Note: The /server/info endpoint exposes sensitive configuration and system details. It must be protected by security handlers. In a typical light-4j deployment, this endpoint is secured requiring a special bootstrap token (e.g., from light-controller).

Module Registration

Modules in light-4j can register themselves to be included in the server info output. This allows the info endpoint to display configuration for custom or contributed modules.

To register a module:

import com.networknt.utility.ModuleRegistry;
import com.networknt.config.Config;

// Registering a module
ModuleRegistry.registerModule(
    MyHandler.class.getName(), 
    Config.getInstance().getJsonMapConfigNoCache(MyHandler.CONFIG_NAME), 
    null
);

Masking Sensitive Data

Configuration files often contain sensitive data (passwords, secrets). When registering a module, you can provide a list of keys to mask in the output.

List<String> masks = new ArrayList<>();
masks.add("trustPass");
masks.add("keyPass");
masks.add("clientSecret");

ModuleRegistry.registerModule(
    Client.class.getName(), 
    Config.getInstance().getJsonMapConfigNoCache(clientConfigName), 
    masks
);

Downstream Info Aggregation

For components like light-gateway, http-sidecar, or light-proxy, the server info endpoint can be configured to aggregate information from a downstream service.

  • downstreamEnabled: When set to true, the handler will attempt to fetch server info from the configured downstream service.
  • downstreamHost: The base URL of the downstream service.
  • downstreamPath: The path to the server info endpoint on the downstream service.

If the downstream service does not implement the info endpoint, the handler usually fails gracefully or returns its own info depending on the implementation specifics (e.g., in light-proxy scenarios).

Sample Output

The output is a JSON object structured as follows:

{
  "environment": {
    "host": {
      "ip": "10.0.0.5",
      "hostname": "service-pod-1",
      "dns": "localhost"
    },
    "runtime": {
      "availableProcessors": 4,
      "freeMemory": 12345678,
      "totalMemory": 23456789,
      "maxMemory": 1234567890
    },
    "system": {
      "javaVendor": "Oracle Corporation",
      "javaVersion": "17.0.1",
      "osName": "Linux",
      "osVersion": "5.4.0",
      "userTimezone": "UTC"
    }
  },
  "specification": { ... }, 
  "component": {
      "com.networknt.info.ServerInfoConfig": {
          "enableServerInfo": true,
          "downstreamEnabled": false,
          ...
      },
      ... other modules ...
  }
}

Logger Handler

The logger-handler module provides a suite of administrative endpoints to manage logger levels and retrieve log contents at runtime. This allows developers and operators to troubleshoot issues in a running system by adjusting verbosity or inspecting logs without requiring a restart.

Note: These administrative features are powerful. In production environments, they must be protected by security handlers (e.g., OAuth 2.0 JWT verification with specific scopes) to prevent unauthorized access. It is highly recommended to use the Light Controller or Light Portal for centralized management.

Features

  • Runtime Level Adjustment: Change logging levels for existing loggers or create new ones for specific packages/classes.
  • Log Content Inspection: Retrieve log entries directly from service instances for UI display or analysis.
  • Pass-Through Support: In gateway or sidecar deployments, these handlers can pass requests through to backend services (even if they use different frameworks like Spring Boot).
  • Auto-Registration: The module automatically registers its configuration with the ModuleRegistry for visibility.

Configuration (logging.yml)

The behavior of the logging handlers is controlled via logging.yml.

# Logging endpoint configuration
---
# Indicate if the logging info is enabled or not.
enabled: ${logging.enabled:true}

# Default time period backward in milliseconds for log content retrieval.
# Default is 10 minutes (600,000 ms).
logStart: ${logging.logStart:600000}

# Downstream configuration (useful in sidecar/gateway scenarios)
downstreamEnabled: ${logging.downstreamEnabled:false}

# Downstream API host (e.g., the backend service IP)
downstreamHost: ${logging.downstreamHost:http://localhost:8081}

# Framework of the downstream service (Light4j, SpringBoot, etc.)
downstreamFramework: ${logging.downstreamFramework:Light4j}

Set Up

To enable the logging endpoints, add the following to your handler.yml:

handlers:
  - com.networknt.logging.handler.LoggerGetHandler@getLogger
  - com.networknt.logging.handler.LoggerPostHandler@postLogger
  - com.networknt.logging.handler.LoggerGetNameHandler@getLoggerName
  - com.networknt.logging.handler.LoggerGetLogContentsHandler@getLogContents

paths:
  - path: '/adm/logger'
    method: 'GET'
    exec:
      - admin  # Security handler
      - getLogger
  - path: '/adm/logger'
    method: 'POST'
    exec:
      - admin
      - postLogger
  - path: '/adm/logger/{loggerName}'
    method: 'GET'
    exec:
      - getLoggerName
  - path: '/adm/logger/content'
    method: 'GET'
    exec:
      - admin
      - getLogContents

Usage

1. Get Logger Levels

Retrieves all configured loggers and their current levels.

Endpoint: GET /adm/logger

curl -k https://localhost:8443/adm/logger

Example Response:

[
  {"name": "ROOT", "level": "ERROR"},
  {"name": "com.networknt", "level": "TRACE"}
]

2. Update Logger Levels

Changes the logging level for specific loggers.

Endpoint: POST /adm/logger

curl -k -H "Content-Type: application/json" -X POST \
  -d '[{"name": "com.networknt.handler", "level": "DEBUG"}]' \
  https://localhost:8443/adm/logger

3. Get Log Contents

Retrieves actual log messages from the application’s file appenders.

Endpoint: GET /adm/logger/content Query Parameters:

  • loggerName: Filter by logger name.
  • loggerLevel: Filter by minimal level (e.g., TRACE).
  • startTime / endTime: Epoch milliseconds.
  • limit / offset: Pagination controls (default 100/0).
curl -k 'https://localhost:8443/adm/logger/content?loggerLevel=DEBUG&limit=10'

Pass-Through (Sidecars/Gateways)

When using http-sidecar or light-gateway, you can target the backend service’s loggers by adding the X-Adm-PassThrough: true header to your request.

  • If the backend is Light4j, the request is passed as-is.
  • If the backend is SpringBoot, the sidecar transforms the request to target Spring Boot Actuator endpoints (e.g., /actuator/loggers).

Example for Spring Boot Backend:

logging.downstreamEnabled: true
logging.downstreamHost: http://localhost:8080
logging.downstreamFramework: SpringBoot
curl -H "X-Adm-PassThrough: true" https://localhost:9445/adm/logger

Dependency

Add the following to your pom.xml:

<dependency>
    <groupId>com.networknt</groupId>
    <artifactId>logger-handler</artifactId>
    <version>${version.light-4j}</version>
</dependency>

Utility

Ldap Utility

Module

Monad Result

The monad-result module provides a functional way to handle success and failure occurrences in your application logic. It is an implementation of a Result monad, similar to Optional in Java, but designed to carry failure information (error status) instead of just being empty.

This approach promotes cleaner code by avoiding the use of exceptions for expected business logic failures, making the control flow more explicit and easier to follow.

Core Components

The module consists of three main parts:

1. Result

An interface that represents the result of an operation. It can be either a Success or a Failure.

2. Success

An implementation of Result that holds the successful value.

3. Failure

An implementation of Result that holds a com.networknt.status.Status object describing the error.

Usage

Creating Results

You can create success or failure results using the static factory methods:

import com.networknt.monad.Result;
import com.networknt.monad.Success;
import com.networknt.monad.Failure;
import com.networknt.status.Status;

// Create a success result
Result<String> success = Success.of("Hello World");

// Create a failure result with a Status object
Status status = new Status("ERR10001");
Result<String> failure = Failure.of(status);

Transforming Results

The Result interface provides several monadic methods to work with the values without explicitly checking for success or failure:

Map

Applies a function to the value if the result is a success. If it’s a failure, it returns the failure as-is.

Result<Integer> length = success.map(String::length);

FlatMap

Similar to map, but the mapping function returns another Result. This is useful for chaining multiple operations that can fail.

Result<User> user = idResult.flatMap(id -> userService.getUser(id));

Fold

Reduces the Result to a single value by providing functions for both success and failure cases. This is often used at the end of a chain to convert the result into a response body or an external format.

String message = result.fold(
    val -> "Success: " + val,
    fail -> "Error: " + fail.getError().getMessage()
);

Lift

Allows combining two Result instances by applying a function to their internal values. If either result is a failure, it returns a failure.

Result<String> result1 = Success.of("Hello");
Result<String> result2 = Success.of("World");
Result<String> combined = result1.lift(result2, (r1, r2) -> r1 + " " + r2);

Conditional Execution

You can perform actions based on the state of the result using ifSuccess and ifFailure:

result.ifSuccess(val -> System.out.println("Got value: " + val))
      .ifFailure(fail -> System.err.println("Error Status: " + fail.getError()));

Why use Monad Result?

  1. Explicit Error Handling: Failures are part of the method signature and cannot be accidentally ignored like unchecked exceptions.
  2. Improved Readability: Functional chaining (map, flatMap) leads to cleaner logic compared to deeply nested if-else blocks.
  3. Composability: It is easy to combine multiple operations into a single result using flatMap and lift.

Dependency

Add the following to your pom.xml:

<dependency>
    <groupId>com.networknt</groupId>
    <artifactId>monad-result</artifactId>
    <version>${version.light-4j}</version>
</dependency>

Data Mask

In a production environment, various logging statements are written to log files or persistent storage to assist in identifying and resolving issues. Since a broad group of people might have access to these logs, sensitive information such as credit card numbers, SIN numbers, or passwords must be masked before logging for confidentiality and compliance.

The mask module provides a utility to handle these masking requirements across different data formats.

StartupHookProvider

The mask module depends on JsonPath, which allows for easy access to nested elements in JSON strings. To ensure consistency and performance, you should configure JsonPath to use the Jackson parser (which is standard in light-4j) instead of the default json-smart parser.

The light-4j framework provides JsonPathStartupHookProvider for this purpose. You can enable it by updating your service.yml:

singletons:
- com.networknt.server.StartupHookProvider:
    - com.networknt.server.JsonPathStartupHookProvider

When using light-codegen, this provider is often included in the generated service.yml but commented out by default.

Configuration (mask.yml)

The masking behavior is fully configurable via mask.yml. It supports four sections: string, regex, json, and map.

Example mask.yml

---
# Replacement rules for specific keys. 
# Key maps to a map of "regex-to-find": "replacement-string"
string:
  uri:
    "password=[^&]*": "password=******"

# Masking based on regex groups. 
# Matching groups in the value will be replaced by stars (*) of the same length.
regex:
  header:
    Authorization: "^Bearer\\s+(.*)$"

# Masking based on JSON Path. 
# Key maps to a map of "json-path": "mask-expression"
json:
  user:
    "$.password": ""
    "$.creditCard.number": "^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14})$"

Usage

The com.networknt.mask.Mask utility class provides static methods to apply masking based on the configuration above.

1. Mask with String

Used for simple replacements, typically in URIs or query parameters.

// Uses the 'uri' key from the 'string' section in mask.yml
String maskedUri = Mask.maskString("https://localhost?user=admin&password=123", "uri");
// Result: https://localhost?user=admin&password=******

2. Mask with Regex

Replaces the content of matching groups with the * character. This is useful for headers or cookies where you want to preserve the surrounding structure.

// Uses the 'header' key and 'Authorization' name from the 'regex' section
String input = "Bearer eyJhbGciOiJIUzI1...";
String masked = Mask.maskRegex(input, "header", "Authorization");
// Result: Bearer ****************...

3. Mask with JsonPath

The most powerful way to mask JSON data. It supports single values, nested objects, and arrays.

// Uses the 'user' key from the 'json' section
String json = "{\"username\":\"admin\", \"password\":\"secret123\"}";
String maskedJson = Mask.maskJson(json, "user");
// Result: {"username":"admin", "password":"*********"}

Masking Lists/Arrays

The JSON masking logic automatically handles arrays if the JSON Path targets an array element (e.g., $.items[*].id). It ensures that only the values are masked while preserving the array structure.

Dependency

Add the following to your pom.xml:

<dependency>
    <groupId>com.networknt</groupId>
    <artifactId>mask</artifactId>
    <version>${version.light-4j}</version>
</dependency>

Auto-Registration

The mask module registers itself with the ModuleRegistry automatically when the configuration is loaded. You can verify the loaded masking rules through the Server Info endpoint.

Portal Registry

The portal-registry module is a decentralized service registry and discovery implementation designed to replace external tools like Consul or Etcd. It connects directly to the light-controller, which acts as the centralized control panel for the light-portal ecosystem.

Why Portal Registry?

While Consul is a popular choice for service discovery, portal-registry was developed to address several pain points:

  1. Resource Efficiency: Eliminates the need for local Consul agents on every node, reducing infrastructure overhead.
  2. WebSocket-Based Updates: Unlike Consul’s long-polling (blocking queries) for discovery updates, portal-registry uses WebSockets. This significantly reduces thread utilization in cloud environments.
  3. Simpler Procurement: Provides an out-of-the-box solution within the light-platform, avoiding lengthy enterprise software approval cycles often required for third-party tools.

Implementation Guide

To use the portal registry in your light-4j service, follow these steps:

1. Dependency Management

Add the following dependency to your pom.xml:

<dependency>
    <groupId>com.networknt</groupId>
    <artifactId>portal-registry</artifactId>
    <version>${version.light-4j}</version>
</dependency>

2. Enable Registry in server.yml

Ensure that registry support is enabled in your server.yml or via values.yml:

enableRegistry: ${server.enableRegistry:true}

3. Service Configuration (service.yml)

Update your service.yml to use PortalRegistry as the registry implementation. Note the use of the light protocol in the URL configuration.

singletons:
- com.networknt.registry.URL:
  - com.networknt.registry.URLImpl:
      protocol: light
      host: localhost
      port: 8080
      path: portal
      parameters:
        registryRetryPeriod: '30000'
- com.networknt.portal.registry.client.PortalRegistryClient:
  - com.networknt.portal.registry.client.PortalRegistryClientImpl
- com.networknt.registry.Registry:
  - com.networknt.portal.registry.PortalRegistry
- com.networknt.balance.LoadBalance:
  - com.networknt.balance.RoundRobinLoadBalance
- com.networknt.cluster.Cluster:
  - com.networknt.cluster.LightCluster

4. Module Configuration (portal-registry.yml)

Create or update the portal-registry.yml file to configure connectivity to the controller.

---
# Portal URL for accessing controller API. 
portalUrl: ${portalRegistry.portalUrl:https://localhost:8443}

# JWT token for authorized access to the light-controller.
portalToken: ${portalRegistry.portalToken:}

# Max requests before resetting connection (HTTP/2 optimization).
maxReqPerConn: ${portalRegistry.maxReqPerConn:1000000}

# Timing for critical health state and de-registration.
deregisterAfter: ${portalRegistry.deregisterAfter:120000}
checkInterval: ${portalRegistry.checkInterval:10000}

# Health Check Mechanisms
httpCheck: ${portalRegistry.httpCheck:false}
ttlCheck: ${portalRegistry.ttlCheck:true}
healthPath: ${portalRegistry.healthPath:/health/}

Operational Workflow

Registry (Service-Side)

When a service starts, it registers itself with the light-controller. During heartbeats (TTL) or polling (HTTP), the controller maintains the service status. When the service shuts down gracefully, it unregisters itself to remove stale nodes immediately.

Discovery (Consumer-Side)

  1. Initial Query: On the first request to a downstream service, the consumer queries the light-controller for available nodes based on serviceId and environment tags.
  2. Streaming Updates: After the initial lookup, the portal-registry opens a WebSocket connection to the controller. Any changes to the downstream service instances (new nodes, failures, or de-registrations) are pushed to the consumer in real-time to update its local cache.

Auto-Registration & Security

The module automatically registers itself with the ModuleRegistry during configuration loading. This allows you to inspect the registry status and configuration via the Server Info endpoint. Sensitive tokens like portalToken are automatically masked in these logs/endpoints.

Direct Registry

If you don’t have Consul or the Light Portal deployed for service registry and discovery, you can use the built-in DirectRegistry as a temporary solution. It provides a simple way to define service-to-host mappings and allows for an easy transition to more robust enterprise registry solutions later.

There are two main ways to define the mapping between a serviceId and its corresponding hosts:

  1. service.yml: The original method, defined as part of the URLImpl parameters.
  2. direct-registry.yml: The recommended method for dynamic environments like http-sidecar or light-gateway and it supports configuration hot reload.

1. Configuration via service.yml

Defining mappings in service.yml is straightforward but comes with a significant limitation: the configuration cannot be reloaded without restarting the server.

Single URL Mapping

A simple mapping from one serviceId to exactly one service instance.

singletons:
- com.networknt.registry.URL:
  - com.networknt.registry.URLImpl:
      protocol: https
      host: localhost
      port: 8080
      path: direct
      parameters:
        com.networknt.apib-1.0.0: http://localhost:7002
        com.networknt.apic-1.0.0: http://localhost:7003
- com.networknt.registry.Registry:
  - com.networknt.registry.support.DirectRegistry
- com.networknt.balance.LoadBalance:
  - com.networknt.balance.RoundRobinLoadBalance
- com.networknt.cluster.Cluster:
  - com.networknt.cluster.LightCluster

Multiple URL Mapping

For a service with multiple instances, provide a comma-separated list of URLs.

      parameters:
        com.networknt.apib-1.0.0: http://localhost:7002,http://localhost:7005

Using Environment Tags

To support multi-tenancy, you can pass tags (e.g., environment) into the registered URLs.

      parameters:
        com.networknt.portal.command-1.0.0: https://localhost:8440?environment=0000,https://localhost:8441?environment=0001

Note: This is the legacy approach and it is not recommended as hot reload is not supported when serviceId to hosts mapping is defined this way.


When using DirectRegistry in http-sidecar or light-gateway, it is highly recommended to use direct-registry.yml. This file supports the config-reload feature, allowing you to update service mappings without downtime.

To use this method, ensure that the parameters section in the URLImpl configuration within service.yml is either empty or commented out.

Example direct-registry.yml

This file maps serviceId (optionally with an environment tag) to host URLs.

---
# direct-registry.yml
# Mapping between serviceId and hosts (comma-separated).
# If a tag is used, separate it from serviceId using a vertical bar |
directUrls:
  code: http://192.168.1.100:6881,http://192.168.1.101:6881
  token: http://192.168.1.100:6882
  com.networknt.test-1.0.0: http://localhost,https://localhost
  command|0000: https://192.168.1.142:8440
  command|0001: https://192.168.1.142:8441

Mappings can also be provided in JSON or Map String formats via environment variables or the config server within values.yml.


Reloading Configuration

When DirectRegistry initializes, it automatically registers its configuration with the ModuleRegistry. You can use the config-reload API to trigger a reload of direct-registry.yml from the local filesystem or a config server.

Simultaneous Reloads

Because components like RouterHandler site on top of the registry and cache their own mappings, you must reload the related modules simultaneously to ensure consistency.

Example Reload Request:

curl --location --request POST 'https://localhost:8443/adm/modules' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <TOKEN>' \
--data-raw '[
    "com.networknt.router.middleware.PathPrefixServiceHandler",
    "com.networknt.router.RouterHandler",
    "com.networknt.registry.support.DirectRegistry"
]'

Auto-Registration & Visibility

The direct-registry module now implements auto-registration via the DirectRegistryConfig singleton. The current active mappings and configuration parameters can be inspected at runtime via the Server Info endpoint.

Dependency

Add the following to your pom.xml:

<dependency>
    <groupId>com.networknt</groupId>
    <artifactId>registry</artifactId>
    <version>${version.light-4j}</version>
</dependency>

Rule Loader

The rule-loader is a vital infrastructure module in light-4j that enables services to dynamically load and manage business logic rules during startup. It serves as the bridge between the YAML Rule Engine and the specific service endpoints, allowing for flexible, rule-based request and response processing.

Overview

Rules in the light-4j ecosystem are often shared across multiple lines of business or organizations. By centralizing these rules in the light-portal, applications can subscribe to them and have them automatically fetched and instantiated at runtime.

Features

  • Dual Rule Sources: Fetch rules from the light-portal for production environments or load them from a local rules.yml file for offline testing.
  • Startup Integration: Uses a standard StartupHookProvider to ensuring all rules are ready before the server starts accepting traffic.
  • Endpoint-to-Rule Mapping: Flexible mapping of endpoints to specific rule sets (e.g., access control, request transformation, response filtering).
  • Service Dependency Enrichment: Automatically fetches service-specific permissions and enriches rule contexts.
  • Dynamic Action Loading: Automatically discovery and instantiates action classes defined within the rules to prevent runtime errors.
  • Auto-Registration: Automatically registers with ModuleRegistry for runtime configuration inspection.

Configuration (rule-loader.yml)

The rule-loader.yml file defines how and where the rules are fetched.

# Rule Loader Configuration
---
# A flag to enable the rule loader. Default is true.
enabled: ${rule-loader.enabled:true}

# Source of the rules: 'light-portal' or 'config-folder'.
# 'light-portal' fetches from a remote server.
# 'config-folder' expects a rules.yml file in the externalized config directory.
ruleSource: ${rule-loader.ruleSource:light-portal}

# The portal host URL (used when ruleSource is light-portal).
portalHost: ${rule-loader.portalHost:https://localhost}

# The authorization token for connecting to the light-portal.
portalHost: ${rule-loader.portalToken:}

# Endpoint to rules mapping (used when ruleSource is config-folder).
# endpointRules:
#   /v1/pets@get:
#     res-tra:
#       - ruleId: transform-pet-response
#   /v1/orders@post:
#     access-control:
#       - ruleId: check-order-permission

Rule Sources

1. Light Portal (light-portal)

In this mode, the loader interacts with the portal API to fetch the latest rules authorized for the service based on the hostId, apiId, and apiVersion defined in values.yml or server.yml. The fetched rules are cached locally in the target configuration directory as rules.yml for resilience.

2. Config Folder (config-folder)

This mode is ideal for local development or air-gapped environments. The loader looks for a rules.yml file in the configuration directory and uses the endpointRules map defined in rule-loader.yml to link endpoints to rule IDs.

Setup

1. Add Dependency

Include the rule-loader in your pom.xml:

<dependency>
    <groupId>com.networknt</groupId>
    <artifactId>rule-loader</artifactId>
    <version>${version.light-4j}</version>
</dependency>

2. Configure Startup Hook

The RuleLoaderStartupHook must be registered in your service.yml or handler.yml under the startup hooks section.

- com.networknt.server.StartupHookProvider:
  - com.networknt.rule.RuleLoaderStartupHook

Operational Visibility

The rule-loader module utilizes the Singleton pattern for its configuration and automatically registers itself with the ModuleRegistry. You can inspect the current ruleSource, portalHost, and active endpointRules mappings at runtime via the Server Info endpoint.

Action Class discovery

During the initialization phase, the loader iterates through all actions defined in the loaded rules and attempts to instantiate their associated Java classes. This proactive step ensures that all required JAR files are present on the classpath and that the classes are correctly configured before the first request arrives.

HTTP Server

The server module is the entry point of the light-4j framework. It wraps the Undertow core HTTP server and manages its entire lifecycle. This includes initializing configuration loaders, merging status codes, registering the server with the module registry, and orchestrating the request/response handler chain.

Core Responsibilities

  1. Lifecycle Management: Controls server startup, initialization, and graceful shutdown.
  2. Configuration Injection: Orchestrates the loading of configurations from local files or the light-config-server.
  3. Handler Orchestration: Wires together the middleware handler chain and the final business logic handler.
  4. Service Registration: Automatically registers the service instance with registries like Consul or Zookeeper when enabled.
  5. Distributed Security: Integrates with TLS and distributed policy enforcement at the edge.

Server Configuration (server.yml)

The behavior of the server is primarily controlled through server.yml. This configuration is mapped to the ServerConfig class.

Example server.yml

# Server configuration
---
ip: ${server.ip:0.0.0.0}
httpPort: ${server.httpPort:8080}
enableHttp: ${server.enableHttp:false}
httpsPort: ${server.httpsPort:8443}
enableHttps: ${server.enableHttps:true}
enableHttp2: ${server.enableHttp2:true}
keystoreName: ${server.keystoreName:server.keystore}
keystorePass: ${server.keystorePass:password}
keyPass: ${server.keyPass:password}
enableTwoWayTls: ${server.enableTwoWayTls:false}
truststoreName: ${server.truststoreName:server.truststore}
truststorePass: ${server.truststorePass:password}
serviceId: ${server.serviceId:com.networknt.petstore-1.0.0}
enableRegistry: ${server.enableRegistry:false}
dynamicPort: ${server.dynamicPort:false}
minPort: ${server.minPort:2400}
maxPort: ${server.maxPort:2500}
environment: ${server.environment:dev}
buildNumber: ${server.buildNumber:latest}
shutdownGracefulPeriod: ${server.shutdownGracefulPeriod:2000}
bufferSize: ${server.bufferSize:16384}
ioThreads: ${server.ioThreads:4}
workerThreads: ${server.workerThreads:200}
backlog: ${server.backlog:10000}
alwaysSetDate: ${server.alwaysSetDate:true}
serverString: ${server.serverString:L}
allowUnescapedCharactersInUrl: ${server.allowUnescapedCharactersInUrl:false}
maxTransferFileSize: ${server.maxTransferFileSize:1000000}
maskConfigProperties: ${server.maskConfigProperties:true}

Configuration Parameters

ParameterDefaultDescription
ip0.0.0.0Binding address for the server.
httpPort8080Port for HTTP connections (if enabled).
enableHttpfalseFlag to enable HTTP. Recommended only for local testing.
httpsPort8443Port for HTTPS connections.
enableHttpstrueFlag to enable HTTPS.
enableHttp2trueFlag to enable HTTP/2 (requires HTTPS).
keystoreNameserver.keystoreName of the server keystore file.
serviceIdUnique identifier for the service.
dynamicPortfalseIf true, the server binds to an available port in the [minPort, maxPort] range.
bufferSize16384Undertow buffer size.
ioThreads(CPU * 2)Number of IO threads for the XNIO worker.
workerThreads200Number of worker threads for blocking tasks.
shutdownGracefulPeriod2000Wait period in ms for in-flight requests during shutdown.
maskConfigPropertiestrueIf true, sensitive properties are masked in the /server/info response.

Hooks

The server provides a plugin mechanism via hooks, allowing developers to execute custom logic during the startup and shutdown phases.

Startup Hooks

Used for initialization tasks such as setting up database connection pools, warming up caches, or initializing third-party clients. Implement com.networknt.server.StartupHookProvider and register via service.yml.

Shutdown Hooks

Used for cleanup tasks such as closing connections, releasing resources, or deregistering from a control plane. Implement com.networknt.server.ShutdownHookProvider and register via service.yml.

Advanced Features

Dynamic Configuration Loading

Using IConfigLoader (and implementations like DefaultConfigLoader), the server can fetch configurations and secret values from a centralized config server or even a URL on startup.

Performance Tuning

The server exposes low-level Undertow options like backlog, ioThreads, and workerThreads. The light-4j framework is highly optimized for performance and can handle thousands of concurrent requests with minimal memory footprint.

Graceful Shutdown

When a shutdown signal (like SIGTERM) is received, the server:

  1. Unregisters itself from the service registry.
  2. Stops accepting new connections.
  3. Waits for the shutdownGracefulPeriod to allow in-flight requests to complete.
  4. Executes all registered shutdown hooks.

Server Info Registry

The server automatically registers itself and its configuration (sensitive values masked) into the ModuleRegistry. This information is typically exposed via the /server/info endpoint if the InfoHandler is in the chain.

Registry Integration

When enableRegistry is true, the server uses the serviceId, ip, and the bound port to register its location. If dynamicPort is active, it explores the port range until it finds an available one, then uses that specific port for registration, enabling seamless scaling in container orchestrators.

Light-rest-4j

Access Control

The AccessControlHandler is a business middleware handler designed for the light-rest-4j framework. It provides fine-grained, rule-based authorization at the endpoint level, allowing developers to define complex access policies that go beyond simple scope-based security.

Overview

Unlike standard security handlers that verify tokens and scopes, the Access Control handler interacts with the Rule Engine to evaluate business-specific logic. It typically runs late in the middleware chain, after technical concerns like security and validation are handled, and right before the request reaches the business logic or proxy.

Key Features

  • Rule-Based Evaluation: Leverage the Power of the YAML-based Rule Engine.
  • Dynamic Configuration: Supports hot-reloading of both configuration and rules.
  • Flexible Logic: Choose between any or all logic when multiple rules are applied to an endpoint.
  • Path Skipping: Easily bypass checks for specific path prefixes.

Configuration (access-control.yml)

The handler is configured via access-control.yml, which is mapped to the AccessControlConfig class.

# Access Control Handler Configuration
---
# Enable or disable the handler
enabled: ${access-control.enabled:true}

# If there are multiple rules for an endpoint, how to combine them.
# any: access is granted if any rule passes.
# all: access is granted only if all rules pass.
accessRuleLogic: ${access-control.accessRuleLogic:any}

# If no rules are defined for an endpoint, should access be denied?
defaultDeny: ${access-control.defaultDeny:true}

# List of path prefixes to skip access control checks.
skipPathPrefixes:
  - /health
  - /server/info

Configuration Parameters

ParameterDefaultDescription
enabledtrueGlobally enables or disables the handler.
accessRuleLogicanyDetermines evaluation logic for multiple rules (any | all).
defaultDenytrueIf true, endpoints without defined rules will return an error.
skipPathPrefixes[]Requests to these paths skip authorization checks entirely.

How it Works

1. Rule Loading

Rules are loaded during server startup via the RuleLoaderStartupHook. This hook caches the rules in memory for high-performance evaluation.

2. Request Context

When a request arrives, the handler extracts context to build a Rule Engine Payload:

  • Audit Info: User information, client ID, and the resolved OpenApi endpoint.
  • Request Headers: Full map of HTTP headers.
  • Parameters: Combined query and path parameters.
  • Request Body: If a body handler is present and the method is POST/PUT/PATCH.

3. Evaluation

The handler looks up the rules associated with the current OpenApi endpoint.

  • If rules exist, it iterates through them using the configured accessRuleLogic.
  • Rules are executed by the RuleEngine, which returns a result (true/false) and optional error details.

4. Enforcement

  • If the result is Success, the request proceeds to the next handler.
  • If the result is Failure, the handler sets an error status (default ERR10067) and stops the chain.

Hot Reload

The implementation supports hot-reloading through the standardized AccessControlConfig.reload() mechanism.

  • When a configuration change is detected at runtime, the singleton AccessControlConfig instance is updated.
  • The AccessControlHandler loads the latest configuration at the start of every request, ensuring zero-downtime updates to authorization policies.

OpenAPI Meta

The OpenApiHandler is a core middleware handler in the light-rest-4j framework. It is responsible for parsing the OpenAPI specification, matching the incoming request to a specific operation defined in the spec, and attaching that metadata to the request context.

Overview

The OpenApiHandler acts as the “brain” for RESTful services. By identifying the exact endpoint and operation (e.g., GET /pets/{petId}) at the start of the request chain, it enables subsequent handlers—such as Security, Validator, and Metrics—to perform their tasks efficiently without re-parsing the request or the specification.

Key Features

  • Operation Identification: Matches URI and HTTP method to OpenAPI operations.
  • Support for Multiple Specs: Can host multiple OpenAPI specifications in a single instance using path-based routing.
  • Parameter Deserialization: Correctly parses query, path, header, and cookie parameters according to the OpenAPI 3.0 styles.
  • Specification Injection: Supports merging a common “inject” specification (e.g., for administrative endpoints) into the main spec.
  • Hot Reload: Automatically detects and applies changes to its configuration without downtime.

Configuration (openapi-handler.yml)

The handler’s behavior is governed by the openapi-handler.yml file, which maps to OpenApiHandlerConfig.

# OpenAPI Handler Configuration
---
# An indicator to allow multiple openapi specifications.
# Default to false which only allow one spec named openapi.yml/yaml/json.
multipleSpec: ${openapi-handler.multipleSpec:false}

# To allow the call to pass through the handler even if the path is not found in the spec.
# Useful for gateways where some APIs might not have specs deployed.
ignoreInvalidPath: ${openapi-handler.ignoreInvalidPath:false}

# Path to spec mapping. Required if multipleSpec is true.
# The key is the base path and the value is the specification name.
pathSpecMapping:
  v1: openapi-v1
  v2: openapi-v2

Configuration Parameters

ParameterDefaultDescription
multipleSpecfalseWhen true, the handler uses pathSpecMapping to support multiple APIs.
ignoreInvalidPathfalseIf true, requests with no matching path in the spec proceed to the next handler instead of returning an error.
pathSpecMapping{}A map where keys are URI base paths and values are the names of the corresponding spec files.

How it Works

1. Specification Loading

At startup, the handler loads the specification(s). If multipleSpec is disabled, it looks for openapi.yml. If enabled, it loads all specs defined in pathSpecMapping. It also checks for openapi-inject.yml to merge common definitions.

2. Request Matching

For every incoming request, the handler:

  1. Normalizes the request path.
  2. If multipleSpec is on, it determines which specification to use based on the path prefix.
  3. Finds the matching template in the OpenAPI paths (e.g., matching /pets/123 to /pets/{petId}).
  4. Resolves the HTTP method to the specific Operation object.

3. Parameter Deserialization

The handler uses the ParameterDeserializer to extract values from the request according to the styles defined in the OpenAPI spec (e.g., matrix, label, form). These deserialized values are attached to the exchange using specific attachment keys.

4. Audit Info Integration

The handler attaches the following to the auditInfo object:

  • endpoint: The resolved endpoint string (e.g., /pets/{petId}@get).
  • openapi_operation: The full OpenApiOperation object, containing the path, method, and operation metadata.

Hot Reload

The OpenApiHandler supports hot-reloading through a thread-safe check in its handleRequest method.

  • It leverages OpenApiHandlerConfig.load() which returns a cached singleton.
  • If the configuration is updated (e.g., via a config server or manual trigger), the handler detects the change, re-initializes its internal OpenApiHelper state, and builds the new mapping logic immediately.

Performance Optimization

For the most common use case (99%) where a service has only one specification, the handler bypasses the mapping logic and uses a high-performance direct reference to the OpenApiHelper.

OpenAPI Security


description: OpenAPI Security Module

OpenAPI Security

The openapi-security module is a fundamental component of the light-rest-4j framework, specifically designed to protect RESTful APIs defined with OpenAPI Specification 3.0. It provides several middleware handlers that integrate with the framework’s security infrastructure to ensure that only authorized requests reach your business logic.

Overview

Modern web services often require robust security mechanisms such as OAuth 2.0. The openapi-security module simplifies the implementation of these standards by providing out-of-the-box handlers for token verification and authorization based on the OpenAPI specification.

Core Handlers

JwtVerifyHandler

The JwtVerifyHandler is the most commonly used handler in this module. It performs the following tasks:

  • Token Verification: Validates the signature and expiration of the OAuth 2.0 JWT access token.
  • Scope Authorization: Automatically checks the scope or scp claim in the JWT against the required scopes defined for each operation in the OpenAPI specification.
  • Integration: Works seamlessly with openapi-meta to identify the current operation and its security requirements.

SimpleJwtVerifyHandler

The SimpleJwtVerifyHandler is a lightweight alternative to the standard JwtVerifyHandler. It is used when scope validation is not required. It still verifies the JWT signature and expiration but skips the complex authorization checks against the OpenAPI spec.

SwtVerifyHandler

The SwtVerifyHandler is designed for Simple Web Tokens (SWT). Unlike JWTs, which are self-contained, SWTs often require token introspection against an OAuth 2.0 provider. This handler manages the introspection process and validates the token’s validity and associated scopes.

Configuration

The security handlers are primarily configured via security.yml. Key configuration options include:

  • enableVerifyJwt: Toggle for JWT verification.
  • enableVerifyScope: Toggle for scope-based authorization.
  • jwt: Configuration for JWT verification (JWK URLs, certificates, etc.).
  • swt: Configuration for SWT introspection.
  • skipPathPrefixes: A list of path prefixes that should bypass security checks (e.g., /health, /info).
  • passThroughClaims: Enables passing specific claims from the token into request headers for downstream use.

Unified Security

For complex scenarios where multiple security methods (Bearer, Basic, ApiKey) need to be supported simultaneously within a single gateway or service, the UnifiedSecurityHandler (from the unified-security module) can be used to coordinate these different handlers based on the request path and headers.

Hot Reload Support

All handlers in the openapi-security module support hot-reloading of their configurations. If the security.yml or related configuration files are updated on the file system, the handlers will automatically detect the changes and re-initialize their verifiers without requiring a server restart.

Integration with OpenAPI Meta

The openapi-security handlers depend on the OpenApiHandler (from the openapi-meta module) being placed earlier in the request chain. The OpenApiHandler identifies the matching operation in the specification and attaches it to the request, which the security handlers then use for validation.

Best Practices

  1. Enable Scope Verification: Always define required scopes in your OpenAPI specification and ensure enableVerifyScope is set to true.
  2. Use Skip Paths Sparingly: Only skip security for public-facing informational endpoints.
  3. Secure Configuration: Use the encrypted configuration feature of light-4j to protect sensitive information like client secrets or certificate passwords.

OpenAPI Validator


description: OpenAPI Validator Module

OpenAPI Validator

The openapi-validator module provides comprehensive request and response validation against an OpenAPI Specification 3.0. It ensures that incoming requests and outgoing responses adhere to the schemas, parameters, and constraints defined in your API specification.

Overview

In a contract-first development approach, the OpenAPI specification serves as the “source of truth.” The openapi-validator middleware automates the enforcement of this contract, reducing the need for manual validation logic in your business handlers and improving API reliability.

Core Components

ValidatorHandler

The main middleware entry point. It identifies whether validation is required for the current request and coordinates the RequestValidator and ResponseValidator.

RequestValidator

Validates all aspects of an incoming HTTP request:

  • Path Parameters: Checks if path variables match the spec.
  • Query Parameters: Validates presence, type, and constraints of query strings.
  • Header Parameters: Validates required headers and their values.
  • Cookie Parameters: Validates cookie values if defined.
  • Request Body: Validates the JSON payload against the operation’s requestBody schema.

ResponseValidator

Validates the outgoing response from the server. This is typically disabled in production for performance reasons but is invaluable during development and testing to ensure the server respects its own contract.

SchemaValidator

The underlying engine (built on networknt/json-schema-validator) that performs the actual JSON Schema validation for bodies and complex parameters.

Configuration

The module is configured via openapi-validator.yml.

Key Properties

PropertyDefaultDescription
enabledtrueGlobally enables or disables the validator.
logErrortrueIf true, validation errors are logged to the console/file.
legacyPathTypefalseIf true, uses the legacy dot-separated path format in error messages instead of JSON Pointers.
skipBodyValidationfalseUseful for gateways or proxies that want to validate headers/parameters but pass the body through without parsing.
validateResponsefalseEnables validation of outgoing responses.
handleNullableFieldtrueIf true, treats fields explicitly marked as nullable: true in the spec correctly.
skipPathPrefixes[]A list of path prefixes to skip validation for (e.g., /health, /info).

Features

Hot Reload Support

The validator supports hot-reloading. If the openapi-validator.yml or the underlying openapi.yml specification is updated on the filesystem, the handler will automatically detect the change and re-initialize the internal validators without a server restart.

Multiple Specification Support

For gateway use cases where a single server might handle multiple APIs, the validator can maintain separate validation contexts for different path prefixes, each associated with its own OpenAPI specification.

Integration with BodyHandler

The RequestValidator automatically retrieves the parsed body from the BodyHandler. To validate the request body, ensure that BodyHandler is placed before ValidatorHandler in your handler chain.

Error Handling

When validation fails, the handler returns a standardized error response with a 400 Bad Request status (for requests) or logs an error (for responses). The error body follows the standard light-4j status format, including a unique error code and a descriptive message pointing to the specific field that failed validation.

Best Practices

  1. Development vs. Production: Always enable validateResponse during development and CI/CD testing, but consider disabling it in production for high-throughput services.
  2. Contract-First: Keep your openapi.yml accurate. The validator is only as good as the specification it follows.
  3. Gateway Optimization: Use skipBodyValidation: true in gateways if the backend service is also performing validation, to save on CPU cycles spent parsing large JSON payloads twice.

Specification


description: Specification Module

Specification Module

The specification module in light-rest-4j provides a set of handlers to serve and display the API specification (Swagger/OpenAPI) of the service. This is particularly useful for exposing documentation endpoints directly from the running service.

Overview

Exposing the API contract via the service itself ensures that the documentation is always in sync with the deployed version. The specification module provides handlers for:

  • Serving the raw specification file (e.g., openapi.yaml).
  • Rendering a Swagger UI instance to interact with the API.
  • Serving a favicon for the UI.

Components

SpecDisplayHandler

Serves the raw content of the specification file. It supports different content types (defaulting to text/yaml) and loads the file directly from the filesystem or configuration folder.

SpecSwaggerUIHandler

Renders a simple HTML page that embeds Swagger UI (via CDN). It is pre-configured to point to the /spec.yaml endpoint (served by SpecDisplayHandler) to load the API definition.

FaviconHandler

A utility handler that serves a favicon.ico file, commonly requested by browsers when accessing the Swagger UI.

Configuration

The module is configured via specification.yml.

Properties

PropertyDefaultDescription
fileNameopenapi.yamlThe path and name of the specification file to be served.
contentTypetext/yamlThe MIME type to be used when serving the specification file.

Features

Hot Reload support

The specification module fully supports standardized hot-reloading. If the specification.yml is updated, the handlers will automatically refresh their internal configuration without requiring a server restart.

Integration with ModuleRegistry

All handlers in this module register themselves with the ModuleRegistry on startup. This allows administrators to verify the loaded configuration via the /server/info endpoint.

Usage

To use these handlers, you need to register them in your handler.yml.

Example handler.yml registration:

handlers:
  - com.networknt.specification.SpecDisplayHandler@spec
  - com.networknt.specification.SpecSwaggerUIHandler@swagger
  - com.networknt.specification.FaviconHandler@favicon

paths:
  - path: '/spec.yaml'
    method: 'get'
    handler:
      - spec
  - path: '/specui'
    method: 'get'
    handler:
      - swagger
  - path: '/favicon.ico'
    method: 'get'
    handler:
      - favicon

Security Note

Since the specification handlers expose internal API details, it is recommended to protect these endpoints using the AccessControlHandler or similar security mechanisms if the documentation should not be publicly accessible.

Light-hybrid-4j

RPC Router

The rpc-router is a core module of the light-hybrid-4j framework. It provides a high-performance routing and validation mechanism that enables a single server instance to host multiple, independent service handlers (RPC-style).

Overview

In the light-hybrid-4j architecture, the rpc-router serves as the primary dispatcher for incoming requests. Unlike traditional RESTful routing based on URL paths and HTTP verbs, the RPC router typically uses a single endpoint (e.g., /api/json) and determines the target logic based on a serviceId (or cmd) specified in the request payload.

Key Responsibilities:

  • Service Discovery: Dynamically discovering handlers annotated with @ServiceHandler at startup.
  • Request Dispatching: Mapping incoming JSON or Form payloads to the correct HybridHandler.
  • Schema Validation: Validating request payloads against JSON schemas defined in spec.yaml files.
  • Specification Management: Merging multiple spec.yaml files from various service JARs into a single runtime context.
  • Configuration Management: Providing a standardized, hot-reloadable configuration via rpc-router.yml.

Core Components

1. SchemaHandler

The SchemaHandler is a middleware handler that performs the initial processing of RPC requests.

  • Spec Loading: At startup, it scans the classpath for all instances of spec.yaml and merges them.
  • Validation: For every request, it identifies the serviceId, retrieves the associated schema, and validates the data portion of the payload.
  • Hot Reload: Supports refreshing the merged specifications at runtime without a server restart.

2. JsonHandler

The JsonHandler is the final dispatcher in the chain for JSON-based RPC calls.

  • Service Execution: It retrieves the pre-parsed serviceId and data from the exchange attachments (populated by SchemaHandler) and invokes the corresponding HybridHandler.handle() method.

3. RpcRouterConfig

This class manages the configuration found in rpc-router.yml. It supports:

  • Standardized Loading: Singleton-based access via RpcRouterConfig.load().
  • Hot Reload: Thread-safe configuration refreshing via RpcRouterConfig.reload().
  • Module Info: Automatic registration with the ModuleRegistry for runtime monitoring.

4. RpcStartupHookProvider

A startup hook that uses ClassGraph to scan configured packages for any class implementing HybridHandler and bearing the @ServiceHandler annotation.

Configuration (rpc-router.yml)

PropertyDefaultDescription
handlerPackages[]List of package prefixes to scan for service handlers.
jsonPath/api/jsonThe endpoint for JSON-based RPC requests.
formPath/api/formThe endpoint for Form-based RPC requests.
registerServicefalseIf enabled, registers each discovered service ID with the discovery registry (e.g., Consul).

Request Structure

A typical RPC request to the rpc-router looks as follows:

{
  "host": "lightapi.net",
  "service": "petstore",
  "action": "getPetById",
  "version": "1.0.0",
  "data": {
    "id": 123
  }
}

The router constructs the internal serviceId as host/service/action/version (e.g., lightapi.net/petstore/getPetById/1.0.0) to locate the handler.

Implementation Example

1. The Service Handler

@ServiceHandler(id = "lightapi.net/petstore/getPetById/1.0.0")
public class GetPetById implements HybridHandler {
    @Override
    public ByteBuffer handle(HttpServerExchange exchange, Object data) {
        Map<String, Object> params = (Map<String, Object>) data;
        // Business logic...
        return NioUtils.toByteBuffer("{\"id\": 123, \"name\": \"Fluffy\"}");
    }
}

2. The Specification (spec.yaml)

Place this in src/main/resources of your service module:

host: lightapi.net
service: petstore
action:
  - name: getPetById
    version: 1.0.0
    handler: getPetById
    request:
      schema:
        type: object
        properties:
          id: { type: integer }
        required: [id]

Best Practices

  1. Restrict Scanning: Always specify handlerPackages in rpc-router.yml to minimize startup time.
  2. Contract-First: Define your spec.yaml rigorously. The router uses these schemas to protect your handlers from invalid data.
  3. Hot Reload: Use the /server/info endpoint to verify that your configuration and specifications have been updated correctly after a reload.

RPC Security

The rpc-security module in light-hybrid-4j provides security handlers specifically designed for the hybrid framework’s routing mechanism. It extends the core security capabilities of light-4j to work seamlessly with the rpc-router’s service dispatching model.

Overview

In light-hybrid-4j, requests are routed based on a service ID in the payload (e.g., lightapi.net/petstore/getPetById/1.0.0) rather than URL paths. This difference requires specialized security handlers that can:

  1. Verify JWT tokens protecting the RPC endpoint.
  2. Extract required scopes from the target service’s schema (specifically the spec.yaml loaded by the rpc-router).
  3. Authorize the request by comparing the token’s scopes against the service’s required scopes.

Core Components

1. HybridJwtVerifyHandler

This handler extends AbstractJwtVerifyHandler to provide JWT validation for hybrid services.

  • Audit Info Integration: It expects the SchemaHandler (from rpc-router) to have already parsed the request and populated the auditInfo attachment map with the HYBRID_SERVICE_MAP.
  • Dynamic Scope Resolution: Instead of hardcoding scopes or using Swagger endpoints, it retrieves the required scopes directly from the scope property defined in the service’s spec.yaml.
  • Skip Auth Support: It respects the skipAuth flag in the service specification, allowing individual handlers to be public while others remain protected.

2. AccessControlHandler

Provides fine-grained access control based on rule definitions.

  • Rule-Based Access: Can enforce complex authorization rules (e.g., “deny if IP is X” or “allow if time is Y”) beyond simple RBAC.
  • Runtime Configuration: Supports hot-reloading of access control rules and settings via access-control.yml.

Configuration

This module relies on security.yml (shared with the core framework) and specific service definitions in spec.yaml.

security.yml

Standard configuration for JWT verification:

enableVerifyJwt: true
enableVerifyScope: true
enableJwtCache: true

access-control.yml

Configuration for the AccessControlHandler:

enabled: true
accessRuleLogic: 'any' # or 'all'
defaultDeny: true

Usage Example

1. Register Handlers

In your handler.yml, add the security handlers to your chain after the SchemaHandler but before the JsonHandler. The SchemaHandler is required first to resolve the service definition.

handlers:
  - com.networknt.rpc.router.SchemaHandler@schema
  - com.networknt.rpc.security.HybridJwtVerifyHandler@jwt
  - com.networknt.rpc.router.JsonHandler@json

paths:
  - path: '/api/json'
    method: 'post'
    handler:
      - schema
      - jwt
      - json

2. Define Security in spec.yaml

In your hybrid service’s spec.yaml file, define the scope required to access the action, or set skipAuth to true for public endpoints.

Example: Protected Endpoint

service: petstore
action:
  - name: getPetById
    version: 1.0.0
    handler: getPetById
    scope: "petstore.r" 
    request: ...
  • The HybridJwtVerifyHandler will extract petstore.r and ensure the caller’s JWT has this scope.

Example: Public Endpoint

service: petstore
action:
  - name: login
    version: 1.0.0
    handler: loginHandler
    skipAuth: true
    request: ...
  • The handler will skip JWT verification for this action.

Hot Reload

The handlers in this module support hot-reloading. If security.yml or access-control.yml are updated via the config server, the changes will be applied dynamically without restarting the server.

Light-Graphql-4j

Graphql Common

Graphql Validator

Graphql Security

Graphql Router

Light-Kafka

Kafka Common

The kafka-common module is a core shared library for light-kafka and its related microservices (like the kafka-sidecar). It encapsulates common configuration management, serialization/deserialization utilities, and shared constants.

Key Features

  • Centralized Configuration:
    • KafkaProducerConfig: Manages configuration for Kafka producers (topic defaults, serialization formats, audit settings).
    • Hot Reload: Supports dynamic configuration updates for kafka-producer.yml and others via Config.reload().
    • Module Registry: Automatically registers configuration modules with the server’s ModuleRegistry for runtime observability.
  • Shared Utilities:
    • LightSchemaRegistryClient: A lightweight client for interacting with the Confluent Schema Registry.
    • AvroConverter & AvroDeserializer: Helpers for handling Avro data formats.
    • KafkaConfigUtils: Utilities for parsing and mapping configuration properties.

Configuration Classes

KafkaProducerConfig

Responsible for loading kafka-producer.yml. Key properties include:

  • topic: Default topic name.
  • auditEnabled: Whether to send audit logs.
  • auditTarget: Topic or logfile for audit data.
  • injectOpenTracing: Whether to inject OpenTracing headers.

KafkaConsumerConfig

Responsible for loading kafka-consumer.yml.

  • maxConsumerThreads: Concurrency settings.
  • topic: List of topics to subscribe to.
  • deadLetterEnabled: DLQ configuration.

KafkaStreamsConfig

Responsible for loading kafka-streams.yml.

  • cleanUp: State store cleanup settings.
  • applicationId: Streams application ID.

Integration

Include kafka-common in your service to leverage shared Kafka capabilities:

<dependency>
    <groupId>com.networknt</groupId>
    <artifactId>kafka-common</artifactId>
    <version>${version.light-kafka}</version>
</dependency>

Hot Reload

Configurations in this module invoke ModuleRegistry.registerModule upon load and reload. This ensures that any changes pushed from the config server are immediately reflected in the application state and visible via the server’s info endpoints.

Kafka Consumer

The kafka-consumer module provides a RESTful interface for consuming records from Kafka topics. It abstracts the complexity of the native Kafka Consumer API, handling instance management, thread pooling, and record serialization/deserialization.

Core Components

KafkaConsumerManager

Manages the lifecycle of Kafka consumers.

  • Instance Management: Creates and caches KafkaConsumer instances based on configuration.
  • Threading: Uses KafkaConsumerThreadPoolExecutor to handle concurrent read operations.
  • Task Scheduling: Manages long-polling read tasks using a DelayQueue and ReadTaskSchedulerThread to efficiently handle poll() operations without blocking threads unnecessarily.
  • Auto-Cleanup: A background thread (ExpirationThread) automatically closes idle consumers to reclaim resources.

LightConsumer

An interface defining the contract for consumer implementations, potentially allowing for different underlying consumer strategies (though KafkaConsumerManager is the primary implementation).

KafkaConsumerReadTask

Encapsulates a single read request. It iterates over the Kafka consumer records, buffering them until the response size criteria (min/max bytes) are met or a timeout occurs.

Configuration

This module relies on kafka-consumer.yml, managed by KafkaConsumerConfig (from the kafka-common module).

Key Settings:

  • maxConsumerThreads: Controls the thread pool size for consumer operations.
  • server.id: Unique identifier for the server instance, used for consumer naming.
  • consumer.instance.timeout.ms: Idle timeout for consumer instances.

Usage

This module is typically used by the kafka-sidecar or other microservices that need to expose Kafka consumption over HTTP/REST.

The KafkaConsumerManager is usually initialized at application startup:

KafkaConsumerConfig config = KafkaConsumerConfig.load();
KafkaConsumerManager manager = new KafkaConsumerManager(config);

Kafka Producer


description: Kafka Producer Module

Kafka Producer

The kafka-producer module provides an abstraction for key features of the light-kafka ecosystem, including auditing, schema validation, and header injection. It supports publishing to Kafka with both key and value serialization via Confluent Schema Registry (Avro, JSON Schema, Protobuf).

Core Components

SidecarProducer

The primary producer implementation.

  • Schema Integration: Integrated with SchemaRegistryClient to handle serialization of keys and values. Caches schema lookups for performance.
  • Audit Integration: Automatically generates and sends audit records (success/failure) for each produced message if configured.
  • Asynchronous: Returns CompletableFuture<ProduceResponse> for non-blocking operation.
  • Headers: Propagates traceabilityId and correlationId into Kafka message headers for end-to-end tracing.

NativeLightProducer

An interface extension which exposes the underlying KafkaProducer instance.

SerializedKeyAndValue

Helper class that holds the serialized bytes for key and value along with target partition and headers.

Configuration

This module relies on kafka-producer.yml, managed by KafkaProducerConfig (from the kafka-common module).

Key Settings:

  • topic: Default target topic.
  • keyFormat / valueFormat: Serialization format (e.g., jsonschema, avro, string).
  • auditEnabled: Toggle for audit logging.
  • injectOpenTracing: Optional integration with OpenTracing.

Usage

Initialize SidecarProducer at application startup. It automatically loads configuration and registers itself.

NativeLightProducer producer = new SidecarProducer();
producer.open();

// Usage
ProduceRequest request = ...;
producer.produceWithSchema(topic, serviceId, partition, request, headers, auditList);

Kafka Streams

The kafka-streams module provides a lightweight wrapper around the Kafka Streams API, simplifying common tasks such as configuration loading, audit logging, and Dead Letter Queue (DLQ) integration.

Core Components

LightStreams

An interface that helps bootstrap a Kafka Streams application. It typically includes:

  • startStream(): Initializes the KafkaStreams instance with the provided topology and configuration. It automatically adds audit and exception handling sinks to the topology if configured.
  • getKafkaValueByKey(): A utility to query state stores (interactive queries) with retry logic for handling rebalances.
  • getAllKafkaValue(): Queries all values from a state store.

Configuration

This module relies on kafka-streams.yml, managed by KafkaStreamsConfig (from the kafka-common module).

Key Settings:

  • application.id: The unique identifier for the streams application.
  • bootstrap.servers: Kafka cluster connection string.
  • cleanUp: If set to true, the application usually performs a local state store cleanup on startup (useful for resetting state).
  • auditEnabled: When true, an “AuditSink” is added to the topology to capture audit events.
  • deadLetterEnabled: When true, automatically configures DLQ sinks for error handling based on provided metadata.

Usage

To use this module, implement LightStreams or call its default methods from your startup logic:

// Load config
KafkaStreamsConfig config = KafkaStreamsConfig.load();

// Build Topology
Topology topology = ...;

// Start Stream
KafkaStreams streams = startStream(ip, port, topology, config, dlqMap, auditParentNames);

The module automatically handles the registration of the configuration module with the server for runtime observability.

Light-spa-4j

MSAL Exchange Handler

The msal-exchange module in light-spa-4j provides a handler to exchange Microsoft Authentication Library (MSAL) tokens for internal application session cookies. This mechanism effectively serves as a Backend-For-Frontend (BFF) authentication layer for Single Page Applications (SPAs).

Core Components

MsalTokenExchangeHandler

This middleware handler intercepts requests to specific paths (configured via msal-exchange.yml) to perform token exchange or logout operations.

  • Token Exchange: Validates the incoming Microsoft Bearer token, performs a token exchange via the OAuth provider, and sets secure, HTTP-only session cookies (accessToken, refreshToken, csrf, etc.) for subsequent requests.
  • Logout: Clears all session cookies to securely log the user out.
  • Session Management: On subsequent requests, it validates the JWT in the cookie, checks for CSRF token consistency, and handles automatic token renewal if the session is nearing expiration.

MsalExchangeConfig

Configuration class that loads settings from msal-exchange.yml. It supports hot reloading and module registration.

Configuration

The module is configured via msal-exchange.yml.

Key Settings:

  • enabled: Enable or disable the handler.
  • exchangePath: Partial path for triggering token exchange (default: /auth/ms/exchange).
  • logoutPath: Partial path for triggering logout (default: /auth/ms/logout).
  • cookieDomain / cookiePath: Scope configuration for the session cookies.
  • cookieSecure: Whether to mark cookies as Secure (HTTPS only).
  • sessionTimeout: Max age for the session cookies.

Example Configuration:

enabled: true
exchangePath: /auth/ms/exchange
logoutPath: /auth/ms/logout
cookieDomain: localhost
cookiePath: /
cookieSecure: false
sessionTimeout: 3600
rememberMeTimeout: 604800

Stateless Auth Handler

The stateless-auth module in light-spa-4j provides a robust, stateless authentication mechanism for Single Page Applications (SPAs). It handles the OAuth 2.0 Authorization Code flow and manages the resulting tokens using secure, HTTP-only cookies, eliminating the need for client-side storage (like localStorage).

Core Components

StatelessAuthHandler

This middleware implements the Backend-For-Frontend (BFF) pattern:

  • Authorization: Intercepts requests to /authorization, exchanges the authorization code for access and refresh tokens from the OAuth 2.0 provider.
  • Cookie Management: Stores tokens in secure, HTTP-only cookies (accessToken, refreshToken) and exposes non-sensitive user info (id, roles) in JavaScript-readable cookies.
  • CSRF Protection: Generates and validates Double Submit Cookies (CSRF token in header vs. JWT claim) to prevent Cross-Site Request Forgery.
  • Token Renewal: Automatically renews expiring access tokens using the refresh token when the session is nearing timeout (default check at < 1.5 minutes remaining).
  • Logout: Handles requests to /logout by invalidating all session cookies.

StatelessAuthConfig

Configuration class that loads settings from statelessAuth.yml. Supports hot reloading and module registration.

Configuration

The module is configured via statelessAuth.yml.

Key Settings:

  • enabled: Enable or disable the handler.
  • authPath: Path to handle the authorization code callback (default: /authorization).
  • cookieDomain: Domain for the session cookies (e.g., localhost or your domain).
  • cookiePath: Path scope for cookies (default: /).
  • cookieSecure: Set to true for HTTPS environments.
  • sessionTimeout: Expiration time for session cookies.
  • redirectUri: Where to redirect the SPA after successful login.
  • enableHttp2: Whether to use HTTP/2 for backend token calls.

Social Login Support: The configuration also includes sections for configuring social login providers directly if not federating through a central IdP:

  • googlePath, googleClientId, googleRedirectUri
  • facebookPath, facebookClientId
  • githubPath, githubClientId

Light-chaos-monkey

Chaos Monkey


description: Chaos Monkey Module

Chaos Monkey

The chaos-monkey module in light-chaos-monkey allows developers and operators to inject various types of failures (assaults) into a running service to test its resilience. It provides API endpoints to query and update assault configurations dynamically at runtime.

Core Components

ChaosMonkeyConfig

Configuration class that loads settings from chaos-monkey.yml. It defines whether the chaos monkey capability is enabled globally.

ChaosMonkeyGetHandler

A LightHttpHandler that retrieves the current configuration of all registered assault handlers.

  • Endpoint: GET /chaosmonkey (typically configured via openapi.yaml or handler.yml)
  • Response: A JSON object containing the current configurations for Exception, KillApp, Latency, and Memory assaults.

ChaosMonkeyPostHandler

A LightHttpHandler that allows updating a specific assault configuration on the fly.

  • Endpoint: POST /chaosmonkey?assault={handlerClassName}
  • Request: assault query parameter specifying the target handler class (e.g., com.networknt.chaos.LatencyAssaultHandler), and a JSON body matching that handler’s configuration structure.
  • Behavior: Updates the static configuration of the specified assault handler and triggers a re-registration of the module config.

Assault Types

  • ExceptionAssault: Injects exceptions into request handling.
  • KillappAssault: Terminates the application instance (use with caution!).
  • LatencyAssault: Injects artificial delays (latency) into requests.
  • MemoryAssault: Consumes heaps memory to simulate memory pressure/leaks.

Configuration

The module itself is configured via chaos-monkey.yml.

Key Settings:

  • enabled: Global switch to enable or disable the chaos monkey endpoints (default: false).

Example Configuration:

enabled: true

Each assault type has its own specific configuration (e.g., latency-assault.yml, exception-assault.yml) which controls the probability and specifics of that attack.

Exception Assault


description: Exception Assault Handler

Exception Assault

The exception-assault module in light-chaos-monkey allows you to inject random exceptions into your application’s request processing pipeline. This helps verify that your application and its consumers gracefully handle unexpected failures.

Core Components

ExceptionAssaultHandler

The middleware handler responsible for injecting exceptions.

  • Behavior: When enabled and triggered (based on the level configuration), it throws an AssaultException. This exception disrupts the normal request flow, simulating an internal server error or unexpected crash.
  • Bypass: Can be configured to bypass the assault logic (e.g., for specific requests or globally until ready).

ExceptionAssaultConfig

Configuration class that loads settings from exception-assault.yml.

Configuration

The module is configured via exception-assault.yml.

Key Settings:

  • enabled: Enable or disable the handler (default: false).
  • bypass: If true, the assault is skipped even if enabled (default: true). Use this to deploy the handler but keep it inactive until needed.
  • level: The probability of an attack, defined as “1 out of N requests”.
    • level: 5 means approximately 1 in 5 requests (20%) will be attacked.
    • level: 1 means every request is attacked.

Example Configuration:

enabled: true
bypass: false
level: 5

Killapp Assault

The killapp-assault module in light-chaos-monkey allows you to inject a termination assault into your application. When triggered, it gracefully shuts down the server and then terminates the JVM process. This is the most destructive type of assault and should be used with extreme caution.

Core Components

KillappAssaultHandler

The middleware handler responsible for killing the application.

  • Behavior: When enabled and triggered (based on the level configuration), it calls Server.shutdown() to stop the light-4j server and then System.exit(0) to terminate the process.
  • Safety: Ensure that your deployment environment (e.g., Kubernetes, Docker Swarm) is configured to automatically restart the application after it terminates, otherwise the service will remain down.

KillappAssaultConfig

Configuration class that loads settings from killapp-assault.yml.

Configuration

The module is configured via killapp-assault.yml.

Key Settings:

  • enabled: Enable or disable the handler (default: false).
  • bypass: If true, the assault is skipped even if enabled (default: true).
  • level: The probability of an attack, defined as “1 out of N requests”.
    • level: 10 means approximately 1 in 10 requests (10%) will trigger a shutdown.
    • level: 1 means the first request will terminate the app.

Example Configuration:

enabled: true
bypass: false
level: 100

Latency Assault

The latency-assault module in light-chaos-monkey allows you to inject artificial delays (latency) into your application’s request processing. This is useful for testing how your application and its downstream consumers handle slow responses and potential timeouts.

Core Components

LatencyAssaultHandler

The middleware handler responsible for injecting latency.

  • Behavior: When enabled and triggered (based on the level configuration), it calculates a random sleep duration within the configured range and puts the current thread to sleep using Thread.sleep().
  • Trigger: The assault is triggered based on a probability defined by the level.

LatencyAssaultConfig

Configuration class that loads settings from latency-assault.yml.

Configuration

The module is configured via latency-assault.yml.

Key Settings:

  • enabled: Enable or disable the handler (default: false).
  • bypass: If true, the assault is skipped even if enabled (default: true).
  • level: The probability of an attack, defined as “1 out of N requests”.
    • level: 10 means approximately 1 in 10 requests (10%) will be attacked.
    • level: 1 means every request is attacked.
  • latencyRangeStart: The minimum delay in milliseconds (default: 1000).
  • latencyRangeEnd: The maximum delay in milliseconds (default: 3000).

Example Configuration:

enabled: true
bypass: false
level: 5
latencyRangeStart: 500
latencyRangeEnd: 2000

Memory Assault

The memory-assault module in light-chaos-monkey allows you to inject memory pressure into your application. When triggered, it allocates memory in increments until a target threshold is reached, holds that memory for a specified duration, and then releases it. This is useful for testing how your application behaves under low-memory conditions and identifying potential memory-related issues.

Core Components

MemoryAssaultHandler

The middleware handler responsible for consuming memory.

  • Behavior: When enabled and triggered (based on the level configuration), it starts an asynchronous process to “eat” free memory. It incrementally allocates byte arrays until the total memory usage reaches the memoryFillTargetFraction of the maximum heap size.
  • Safety: The handler includes logic to prevent immediate OutOfMemoryErrors by controlling the increment size and respecting the maximum target fraction. It also performs a System.gc() after releasing the allocated memory to encourage the JVM to reclaim the space.

MemoryAssaultConfig

Configuration class that loads settings from memory-assault.yml.

Configuration

The module is configured via memory-assault.yml.

Key Settings:

  • enabled: Enable or disable the handler (default: false).
  • bypass: If true, the assault is skipped even if enabled (default: true).
  • level: The probability of an attack, defined as “1 out of N requests”.
    • level: 10 means approximately 1 in 10 requests (10%) will trigger the memory assault.
    • level: 1 means every request can trigger it (though typically it runs until finished once started).
  • memoryMillisecondsHoldFilledMemory: Duration (in ms) to hold the allocated memory once the target fraction is reached (default: 90000).
  • memoryMillisecondsWaitNextIncrease: Time (in ms) between allocation increments (default: 1000).
  • memoryFillIncrementFraction: Fraction of available free memory to allocate in each increment (default: 0.15).
  • memoryFillTargetFraction: The target fraction of maximum heap memory to occupy (default: 0.25).

Example Configuration:

enabled: true
bypass: false
level: 10
memoryFillTargetFraction: 0.5
memoryMillisecondsHoldFilledMemory: 60000

Light-sws-lambda

Lambda Invoker

The lambda-invoker module provides a way to invoke AWS Lambda functions from a light-4j application. It includes a configuration class and an HTTP handler that can be used to proxy requests to Lambda functions.

Core Components

LambdaInvokerConfig

The configuration class for the Lambda invoker. It loads settings from lambda-invoker.yml.

  • region: The AWS region where the Lambda functions are deployed.
  • endpointOverride: Optional URL to override the default AWS Lambda endpoint.
  • apiCallTimeout: Timeout for the entire API call in milliseconds.
  • apiCallAttemptTimeout: Timeout for each individual API call attempt in milliseconds.
  • maxRetries: Maximum number of retries for the invocation.
  • maxConcurrency: Maximum number of concurrent requests to Lambda.
  • functions: A map of endpoints to Lambda function names or ARNs.
  • metricsInjection: Whether to inject Lambda response time metrics into the metrics handler.

LambdaFunctionHandler

An HTTP handler that proxies requests to AWS Lambda functions based on the configured mapping.

  • Behavior: It converts the incoming HttpServerExchange into an APIGatewayProxyRequestEvent, invokes the configured Lambda function asynchronously using the LambdaAsyncClient, and then converts the APIGatewayProxyResponseEvent back into the HTTP response.
  • Metrics: If enabled, it records the total time spent in the Lambda invocation and injects it into the metrics handler.

Configuration

Example lambda-invoker.yml:

region: us-east-1
apiCallTimeout: 60000
apiCallAttemptTimeout: 20000
maxRetries: 2
maxConcurrency: 50
functions:
  /v1/pets: petstore-function
  /v1/users: user-service-function
metricsInjection: true
metricsName: lambda-response

Usage

To use the LambdaFunctionHandler, add it to your handler.yml chain:

- com.networknt.aws.lambda.LambdaFunctionHandler@lambda

And configure the paths in handler.yml:

paths:
  - path: '/v1/pets'
    method: 'GET'
    handler:
      - lambda

Light-websocket-4j

Websocket Client

WebSocket Router

The WebSocketRouterHandler is a middleware handler designed to route WebSocket connections to downstream services in a microservices architecture. It sits in the request chain and identifies WebSocket handshake requests either by a specific header (service_id) or by matching the request path against configured prefixes.

When a request is identified as a WebSocket request targeted for routing, this handler performs the WebSocket handshake (upgrading the connection) and establishes a proxy connection to the appropriate downstream service. It manages the bi-directional traffic between the client and the downstream service.

Configuration

The configuration for the WebSocket Router Handler is located in websocket-router.yml.

# Light websocket router configuration
# Enable WebSocket Router Handler
enabled: ${websocket-router.enabled:true}
# Map of path prefix to serviceId for routing purposes when service_id header is missing.
pathPrefixService: ${websocket-router.pathPrefixService:}

Example Configuration

# websocket-router.yml
enabled: true
pathPrefixService:
  /ws/chat: chat-service
  /ws/notification: notification-service

Usage

To use the WebSocketRouterHandler, you need to register it in your handler.yml configuration file. It should be placed in the middleware chain where you want to intercept and route WebSocket requests.

1. Register the Handler

Add the fully qualified class name to the handlers list in handler.yml:

handlers:
  - com.networknt.websocket.router.WebSocketRouterHandler@router
  # ... other handlers

2. Configure the Chain

Add the handler alias router or a custom alias if defined to the default chain or specific path chains.

chains:
  default:
    - exception
    - metrics
    - traceability
    - correlation
    - header
    - router

How it Works

  1. Request Interception: The handler checks each incoming request.
  2. Identification:
    • Header-based: Checks for the presence of a service_id header.
    • Path-based: Checks if the request path matches any entry in the pathPrefixService map.
  3. Handshake & Upgrade: If matched, the handler delegates to Undertow’s WebSocketProtocolHandshakeHandler to perform the upgrade.
  4. Routing: Upon successful connection (onConnect), it looks up the downstream service URL using the Cluster and Service discovery mechanism based on the service_id.
  5. Proxying: It establishes a WebSocket connection to the downstream service and pipes messages between the client and the backend.

Channel Management

The WebSocket Router uses a concept of “Channels” to manage client sessions.

  1. Channel Group ID:

    • The router expects a unique identifier for each client connection, typically passed in the x-group-id header (internally WsAttributes.CHANNEL_GROUP_ID).
    • If this header is missing (e.g., standard browser connections), the router automatically generates a unique UUID for the session.
  2. Connection Mapping:

    • Each unique Channel Group ID corresponds to a distinct WebSocket connection to the downstream service.
    • If a client connects with a generated UUID, a new connection is established to the backend service for that specific session.
    • Messages are proxied exclusively between the client’s channel and its corresponding downstream connection.

This ensures that multiple browser tabs or distinct clients are isolated, each communicating with the backend over its own dedicated WebSocket link.

Architecture: Router vs. Proxy

You might observe that the WebSocketRouterHandler functions primarily as a Reverse Proxy: it terminates the client connection and establishes a separate connection to the backend service.

It is named a “Router” because of its role in the system architecture:

  1. Multiplexing: It can route different paths (e.g., /chat, /notification) to different backend services on the same gateway port.
  2. Service Discovery: It dynamically resolves the backend URL using the service_id and the configured Registry/Cluster (e.g., Consul, Kubernetes), rather than proxying to a static IP address.

Thus, while the mechanism is proxying, the function is dynamic routing and load balancing of WebSocket traffic.

WebSocket Handler

The WebSocketHandler is a middleware handler designed to process WebSocket messages on the server side (Light Gateway or Light 4J Service), rather than proxying them to downstream services. It enables the implementation of custom logic, such as Chat bots, GenAI integration, or real-time notifications, directly within the application.

Configuration

The configuration is located in websocket-handler.yml.

FieldTypeDescriptionDefault
enabledbooleanEnable or disable the WebSocket Handler.true
pathPrefixHandlersMap<String, String>A map where keys are path prefixes (e.g., /chat) and values are the fully qualified class names of the handler implementation which must implement com.networknt.websocket.handler.WebSocketApplicationHandler.empty

Example Configuration

# websocket-handler.yml
enabled: true
pathPrefixHandlers:
  /chat: com.networknt.chat.ChatHandler
  /notifications: com.networknt.notify.NotificationHandler

Implementing a Handler

To handle WebSocket connections, you must implement the com.networknt.websocket.handler.WebSocketApplicationHandler interface used in the configuration above.

package com.networknt.chat;

import com.networknt.websocket.handler.WebSocketApplicationHandler;
import io.undertow.websockets.core.*;
import io.undertow.websockets.spi.WebSocketHttpExchange;

public class ChatHandler implements WebSocketApplicationHandler {
    @Override
    public void onConnect(WebSocketHttpExchange exchange, WebSocketChannel channel) {
        channel.getReceiveSetter().set(new AbstractReceiveListener() {
            @Override
            protected void onFullTextMessage(WebSocketChannel channel, BufferedTextMessage message) {
                String data = message.getData();
                // Process message (e.g., send to GenAI, broadcast to other users)
                WebSockets.sendText("Echo: " + data, channel, null);
            }
        });
        channel.resumeReceives();
    }
}

Usage

To use the WebSocketHandler, you need to register it in your handler.yml configuration file.

1. Register the Handler

Add the WebSocketHandler to your handler.yml configuration:

handlers:
  - com.networknt.websocket.handler.WebSocketHandler

2. Add to Chain

Place it in the middleware chain. It should be placed after security if authentication is required.

chains:
  default:
    - exception
    - metrics
    - traceability
    - correlation
    - WebSocketHandler
    # ... other handlers

How It Works

  1. Matching: The handler checks if the request path starts with one of the configured pathPrefixHandlers.
  2. Instantiation: It loads and instantiates the configured handler class singleton at startup.
  3. Upgrade: If a request matches, WebSocketHandler performs the WebSocket handshake (upgrading HTTP to WebSocket).
  4. Delegation: Upon successful connection (onConnect), it delegates control to the matched implementation of WebSocketApplicationHandler.

If the request does not match any configured prefix, it is passed to the next handler in the chain.

Maven Dependency

Ensure you have the module dependency in your pom.xml:

<dependency>
    <groupId>com.networknt</groupId>
    <artifactId>websocket-handler</artifactId>
    <version>${version.light-websocket-4j}</version>
</dependency>

WebSocket Rendezvous

The websocket-rendezvous module provides a specialized WebSocket handler for the “Rendezvous” pattern. This pattern is useful when the Gateway needs to bridge a connection between an external client (e.g., a browser or mobile app) and a backend service that is behind a firewall or NAT and cannot accept incoming connections directly.

In this pattern, both the Client and the Backend service initiate outbound WebSocket connections to the Gateway. The Gateway then “pairs” these two connections based on a shared identifier (channelId) and relays messages between them transparently.

Dependencies

To use this module, add the following dependency to your pom.xml:

<dependency>
    <groupId>com.networknt</groupId>
    <artifactId>websocket-rendezvous</artifactId>
    <version>${version.light-4j}</version>
</dependency>

Configuration

The module is configured via websocket-rendezvous.yml (or values.yml overrides).

The configuration key is websocket-rendezvous.

Config PathDefaultDescription
enabledtrueEnable or disable the handler.
backendPath/connectThe URI path suffix (or segment) used to identify the Backend connection.

Example values.yml configuration:

websocket-rendezvous:
  enabled: true
  backendPath: /connect

Handler Configuration

To use the handler in a Light-Gateway or Light-4j service, register it in your handler.yml or values.yml under handler.handlers and map it to the desired paths.

handler.handlers:
  - com.networknt.websocket.rendezvous.WebSocketRendezvousHandler@rendezvous
  # ... other handlers

handler.paths:
  - path: '/chat'
    method: 'GET'
    exec:
      - rendezvous
  - path: '/connect'
    method: 'GET'
    exec:
      - rendezvous

How It Works

  1. Channel Identification: Both the Client and the Backend must provide a channelId to identify the session. This is typically done via the HTTP header channel-group-id or a query parameter channelId in the WebSocket upgrade request.

  2. Role Detection: The handler determines if an incoming connection is a Client or a Backend based on the request URI.

    • If the request URI contains the configured backendPath (default /connect), it is treated as the Backend.
    • Otherwise, it is treated as the Client.
  3. Pairing Logic:

    • When the Client connects, the Gateway creates a new WsProxyClientPair and waits for the Backend.
    • When the Backend connects (providing the same channelId), the Gateway locates the existing session and pairs the Backend connection with the waiting Client connection.
    • Once paired, any message received from the Client is forwarded to the Backend, and vice-versa.
  4. Message Relaying: The module uses a dedicated WebSocketRendezvousReceiveListener to bridge the traffic efficiently.

Connection Model

The Rendezvous mode uses a 1:1 connection mapping model:

  • For every active Client session (identified by a unique channelId), the Backend must establish a separate WebSocket connection to the Gateway with the same channelId.
  • There is no multiplexing of multiple user sessions over a single Backend connection.
  • If you have N users connected to the Gateway, the Backend needs N corresponding connections to the Gateway to serve them.

Usage Example

Client Request:

GET /chat?channelId=12345 HTTP/1.1
Host: gateway.example.com
Upgrade: websocket
Connection: Upgrade

Backend Request:

GET /chat/connect?channelId=12345 HTTP/1.1
Host: gateway.example.com
Upgrade: websocket
Connection: Upgrade

Note: The Backend connects to a path containing /connect (e.g., /chat/connect). We recommend using /chat/connect (or similar) to differentiate it from the Frontend endpoint /chat. Using the same endpoint for both would cause the Gateway to misidentify the Backend as a second Client.

Why not just /connect? In many setups, /connect is reserved as the HTTP trigger endpoint that the UI calls to instruct the Backend to establish the WebSocket connection. Therefore, the Backend should use a distinct WebSocket path like /chat/connect.

Sequence Diagram

sequenceDiagram
    participant User as User (Browser)
    participant Gateway
    participant Backend as Backend Service

    User->>Gateway: WebSocket Connect (/chat?channelId=123)
    Note over Gateway: Created Client Session (Waiting)

    User->>Gateway: HTTP GET /connect?channelId=123
    Gateway->>Backend: HTTP Proxy request to Backend
    Note over Backend: Received Trigger

    Backend->>Gateway: WebSocket Connect (/chat/connect?channelId=123)
    Note over Gateway: Identified as Backend (via /connect path)
    Note over Gateway: Paired with Client Session

    User->>Gateway: Hello Backend
    Gateway->>Backend: Forward: Hello Backend
    
    Backend->>Gateway: Hello User
    Gateway->>User: Forward: Hello User

Light-genai-4j

light-genai-4j is a library that provides integration with various Generative AI models (LLMs) such as Gemini, OpenAI, Bedrock, and Ollama for the light-4j framework.

Design Decisions

GenAI WebSocket Handler

The genai-websocket-handler module is located in this repository (light-genai-4j) rather than light-websocket-4j.

Reasoning:

  • Dependency Direction: This handler implements specific business logic (managing chat history, session context, and invoking LLM clients) that depends on the core GenAI capabilities provided by light-genai-4j.
  • Separation of Concerns: light-websocket-4j is an infrastructure library responsible for the WebSocket transport layer (connection management, routing, hygiene). light-genai-4j is the domain library responsible for AI interactions.
  • Implementation: This module implements com.networknt.websocket.handler.WebSocketApplicationHandler from light-websocket-4j, effectively bridging the transport layer with the AI domain layer.

Ollama Client

The ollama-client module provides a non-blocking, asynchronous client for interacting with the Ollama API. It is part of the light-genai-4j library and leverages the Undertow HTTP client for efficient communication.

Features

  • Non-blocking I/O: Uses Undertow’s asynchronous HTTP client for high throughput.
  • Streaming Support: Supports streaming responses (NDJSON) from Ollama for chat completions.
  • Configuration: externalizable configuration via ollama.yml.
  • Token Management: N/A (Ollama usually runs locally without auth, but can be proxied).

Configuration

The client is configured via ollama.yml in the src/main/resources/config directory (or externalized config folder).

Properties

PropertyDescriptionDefault
ollamaUrlThe base URL of the Ollama server.http://localhost:11434 (example)
modelThe default model to use for requests.llama3.2 (example)

Example ollama.yml

ollamaUrl: http://localhost:11434
model: llama3.2

Usage

Sync Chat (Non-Streaming)

GenAiClient client = new OllamaClient();
List<ChatMessage> messages = new ArrayList<>();
messages.add(new ChatMessage("user", "Hello, who are you?"));

// Blocking call (not recommended for high concurrency)
String response = client.chat(messages);
System.out.println(response);

Async Chat (Streaming)

GenAiClient client = new OllamaClient();
List<ChatMessage> messages = new ArrayList<>();
messages.add(new ChatMessage("user", "Tell me a story."));

client.chatStream(messages, new StreamCallback() {
    @Override
    public void onEvent(String content) {
        System.out.print(content); // Process token chunk
    }

    @Override
    public void onComplete() {
        System.out.println("\nDone.");
    }

    @Override
    public void onError(Throwable throwable) {
        throwable.printStackTrace();
    }
});

Dependencies

  • light-4j core (client, config)
  • genai-core

OpenAI Client

The OpenAiClient is an implementation of GenAiClient that interacts with the OpenAI API (GPT models). It supports both synchronous chat and asynchronous streaming chat.

Features

  • Synchronous Chat: Sends a prompt to the model and waits for the full response.
  • Streaming Chat: streams the response from the model token by token, suitable for real-time applications.
  • Non-Blocking I/O: Uses XNIO and Undertow’s asynchronous client to prevent blocking threads during I/O operations.

Configuration

The client is configured via openai.yml.

Properties

PropertyDescriptionDefault
urlThe OpenAI API URL for chat completions.https://api.openai.com/v1/chat/completions
modelThe model to use (e.g., gpt-3.5-turbo, gpt-4).null
apiKeyYour OpenAI API key.null

Example openai.yml

url: https://api.openai.com/v1/chat/completions
model: gpt-3.5-turbo
apiKey: your-openai-api-key

Usage

Injection

You can inject the OpenAiClient as a GenAiClient implementation.

GenAiClient client = new OpenAiClient();

Synchronous Chat

List<ChatMessage> messages = new ArrayList<>();
messages.add(new ChatMessage("user", "Hello, OpenAI!"));
String response = client.chat(messages);
System.out.println(response);

Streaming Chat

List<ChatMessage> messages = new ArrayList<>();
messages.add(new ChatMessage("user", "Write a long story."));

client.chatStream(messages, new StreamCallback() {
    @Override
    public void onEvent(String content) {
        System.out.print(content);
    }

    @Override
    public void onComplete() {
        System.out.println("\nDone.");
    }

    @Override
    public void onError(Throwable throwable) {
        throwable.printStackTrace();
    }
});

Gemini Client

The GeminiClient is an implementation of GenAiClient that interacts with Google’s Gemini API. It supports both synchronous chat and asynchronous streaming chat.

Features

  • Synchronous Chat: Sends a prompt to the model and waits for the full response.
  • Streaming Chat: streams the response from the model chunk by chunk, suitable for real-time applications.
  • Non-Blocking I/O: Uses XNIO and Undertow’s asynchronous client to prevent blocking threads during I/O operations.

Configuration

The client is configured via gemini.yml.

Properties

PropertyDescriptionDefault
urlThe Gemini API URL base format.https://generativelanguage.googleapis.com/v1beta/models/%s:generateContent
modelThe model to use (e.g., gemini-pro).null
apiKeyYour Google Cloud API key.null

Example gemini.yml

url: https://generativelanguage.googleapis.com/v1beta/models/%s:generateContent
model: gemini-pro
apiKey: your-google-api-key

Usage

Injection

You can inject the GeminiClient as a GenAiClient implementation.

GenAiClient client = new GeminiClient();

Synchronous Chat

List<ChatMessage> messages = new ArrayList<>();
messages.add(new ChatMessage("user", "Hello, Gemini!"));
String response = client.chat(messages);
System.out.println(response);

Streaming Chat

List<ChatMessage> messages = new ArrayList<>();
messages.add(new ChatMessage("user", "Write a poem."));

client.chatStream(messages, new StreamCallback() {
    @Override
    public void onEvent(String content) {
        System.out.print(content);
    }

    @Override
    public void onComplete() {
        System.out.println("\nDone.");
    }

    @Override
    public void onError(Throwable throwable) {
        throwable.printStackTrace();
    }
});

Bedrock Client

The BedrockClient is an implementation of GenAiClient that interacts with AWS Bedrock. It supports both synchronous chat and asynchronous streaming chat via the AWS SDK for Java 2.x.

Features

  • Synchronous Chat: Sends a prompt to the model and waits for the full response.
  • Streaming Chat: streams the response from the model chunk by chunk, suitable for real-time applications.
  • Non-Blocking I/O: Leverages BedrockRuntimeAsyncClient and JDK CompletableFuture for non-blocking operations.

Configuration

The client is configured via bedrock.yml.

Properties

PropertyDescriptionDefault
regionThe AWS region where Bedrock is enabled (e.g., us-east-1).null
modelIdThe Bedrock model ID to use (e.g., anthropic.claude-v2, amazon.titan-text-express-v1).null

Example bedrock.yml

region: us-east-1
modelId: anthropic.claude-v2

Usage

Injection

You can inject the BedrockClient as a GenAiClient implementation.

GenAiClient client = new BedrockClient();

Synchronous Chat

List<ChatMessage> messages = new ArrayList<>();
messages.add(new ChatMessage("user", "Hello, Bedrock!"));
String response = client.chat(messages);
System.out.println(response);

Streaming Chat

List<ChatMessage> messages = new ArrayList<>();
messages.add(new ChatMessage("user", "Tell me a joke."));

client.chatStream(messages, new StreamCallback() {
    @Override
    public void onEvent(String content) {
        System.out.print(content);
    }

    @Override
    public void onComplete() {
        System.out.println("\nDone.");
    }

    @Override
    public void onError(Throwable throwable) {
        throwable.printStackTrace();
    }
});

AWS Credentials

The BedrockClient uses the DefaultCredentialsProvider chain from the AWS SDK. Prior to using the client, ensure your environment is configured with valid AWS credentials (e.g., via environment variables, ~/.aws/credentials, or IAM roles).

GenAI WebSocket Handler

The genai-websocket-handler module provides a WebSocket-based interface for interacting with Generative AI models via the light-genai-4j library. It manages user sessions, maintains chat history context, and handles the bi-directional stream of messages between the user and the LLM.

Architecture

This module is designed as an implementation of the WebSocketApplicationHandler interface defined in light-websocket-4j. It plugs into the websocket-handler infrastructure to receive upgraded WebSocket connections.

Core Components

  1. GenAiWebSocketHandler: The main entry point. It handles onConnect, manages the WebSocket channel life-cycle, and coordinates message processing.
  2. SessionManager: Responsible for creating, retrieving, and validating user chat sessions.
  3. HistoryManager: Responsible for persisting and retrieving the conversation history required for LLM context.
  4. ModelClient: Wraps the light-genai-4j clients (Gemini, OpenAI, etc.) to invoke the model.

Data Models

ChatMessage

Represents a single message in the conversation.

public class ChatMessage {
    String role;       // "user", "model", "system"
    String content;    // The text content
    long timestamp;    // Epoch timestamp
}

ChatSession

Represents the state of a conversation.

public class ChatSession {
    String sessionId;
    String userId;
    String model;      // The generic model name (e.g., "gemini-pro")
    Map<String, Object> parameters; // Model parameters (temperature, etc.)
}

Storage Interfaces

To support scaling from a single instance to a clustered environment, session and history management are abstracted behind repository interfaces.

ChatSessionRepository

public interface ChatSessionRepository {
    ChatSession createSession(String userId, String model);
    ChatSession getSession(String sessionId);
    void deleteSession(String sessionId);
}

ChatHistoryRepository

public interface ChatHistoryRepository {
    void addMessage(String sessionId, ChatMessage message);
    List<ChatMessage> getHistory(String sessionId);
    void clearHistory(String sessionId);
}

Implementations

In-Memory (Default)

The initial implementation provides in-memory storage using concurrent maps. This is suitable for single-instance deployments or testing.

  • InMemoryChatSessionRepository: Uses ConcurrentHashMap<String, ChatSession>.
  • InMemoryChatHistoryRepository: Uses ConcurrentHashMap<String, Deque<ChatMessage>> (or List).

Future Implementations

The design allows for future implementations without changing the core handler logic:

  • JDBC/RDBMS: Persistent storage for long-term history.
  • Redis: Shared cache for clustered deployments (session affinity not required).
  • Hazelcast: In-memory data grid for distributed caching.

Configuration

The implementation to use can be configured via service.yml (using Light-4J’s Service/Module loading) or dedicated config files.

Example service.yml for In-Memory:

- com.networknt.genai.handler.ChatHistoryRepository:
  - com.networknt.genai.handler.InMemoryChatHistoryRepository

Streaming Behavior

The handler implements a buffering mechanism for the streaming response from the LLM to improve client-side rendering.

Response Buffering

LLM providers usually stream tokens (words or partial words) as they are generated. If these tokens are sent to the client immediately as individual WebSocket frames, simplistic clients that print each frame on a new line will display a fragmented “word-per-line” output.

To resolve this, the GenAiWebSocketHandler buffers incoming tokens and only flushes them to the client when:

  1. A newline character (\n) is detected in the buffer.
  2. The stream from the LLM completes.

This ensures that the client receives complete lines or paragraphs, resulting in a cleaner user experience.

Example

Light-websocket-4j

LLM Chat Server

The llmchat-server is an example application demonstrating how to build a real-time Generative AI chat server using light-genai-4j and light-websocket-4j.

It uses the genai-websocket-handler to manage WebSocket connections and orchestrate interactions with an LLM backend (configured for Ollama by default).

Prerequisites

  • jdk 11 or above
  • maven 3.6.0 or above
  • Ollama running locally (for the default configuration) with a model pulled (e.g., qwen3:14b or llama3).

Configuration

The server configuration is consolidated in src/main/resources/config/values.yml.

Server & Handler

The server runs on HTTP port 8080 and defines two main paths:

  • /: Serves static web resources (the chat UI).
  • /chat: The WebSocket endpoint for chat sessions.
handler.paths:
  - path: '/'
    method: 'GET'
    exec:
      - resource
  - path: '/chat'
    method: 'GET'
    exec:
      - websocket

GenAI Client

Dependencies are injected via service.yml configuration (in values.yml). By default, it uses OllamaClient.

service.singletons:
  - com.networknt.genai.GenAiClient:
    - com.networknt.genai.ollama.OllamaClient

Ollama Configuration

Configures the connection to the Ollama instance.

ollama.ollamaUrl: http://localhost:11434
ollama.model: qwen3:14b

WebSocket Handler

Maps the /chat path to the GenAiWebSocketHandler.

websocket-handler.pathPrefixHandlers:
  /chat: com.networknt.genai.handler.GenAiWebSocketHandler

Running the Server

  1. Start Ollama: Ensure Ollama is running and the model configured in values.yml is available.

    ollama run qwen3:14b
    
  2. Build and Start:

    cd light-example-4j/websocket/llmchat-server
    mvn clean install exec:java
    

Usage

Web UI

Open your browser and navigate to http://localhost:8080. You should see a simple chat interface where you can type messages and receive streaming responses from the LLM.

WebSocket Client

You can also connect using any WebSocket client (like wscat):

wscat -c ws://localhost:8080/chat?userId=user1&model=qwen3:14b

Send a message:

> Hello
< Assistant: Hi
< Assistant: there!
...

The server streams the response token by token (buffered by line/sentence for better display).

LLM Chat Gateway

The llmchat-gateway is an example application that demonstrates how to usage websocket-router to create a gateway for the LLM Chat Server.

It acts as a secure entry point (HTTPS) that proxies WebSocket traffic to the backend chat server.

Architecture

  • Gateway: Listens on HTTPS port 8443. Serves the static UI and routes /chat WebSocket connections to the backend.
  • Backend: llmchat-server running on HTTP port 8080.
  • Routing: Uses WebSocketRouterHandler to forward messages based on path or serviceId. Also uses DirectRegistry for service discovery.

Configuration

The configuration is located in src/main/resources/config/values.yml.

Server & Handler

Configured for HTTPS on port 8443.

server.httpsPort: 8443
server.enableHttp: false
server.enableHttps: true

handler.paths:
  - path: '/'
    method: 'GET'
    exec:
      - resource
  - path: '/chat'
    method: 'GET'
    exec:
      - router

WebSocket Router

Maps requests to the backend service.

websocket-router.pathPrefixService:
  /chat: com.networknt.llmchat-1.0.0

Service Registry

Uses DirectRegistry to locate the backend server (llmchat-server) at http://localhost:8080.

service.singletons:
  - com.networknt.registry.Registry:
    - com.networknt.registry.support.DirectRegistry

direct-registry:
  com.networknt.llmchat-1.0.0:
    - http://localhost:8080

Running the Example

  1. Start Ollama: Ensure Ollama is running.
  2. Start Backend: From light-example-4j/websocket/llmchat-server:
    mvn exec:java
    
  3. Start Gateway: From light-example-4j/websocket/llmchat-gateway:
    mvn exec:java
    
    (Note: ensure you have built it first with mvn clean install)

Usage

Open your browser and navigate to https://localhost:8443.

  • You might see a security warning because the server.keystore uses a self-signed certificate. Accept it to proceed.
  • The chat interface is served from the gateway.
  • When you click “Connect”, it opens a secure WebSocket (wss://) to the gateway.
  • The gateway routes frames to llmchat-server, which invokes the LLM and streams the response back.