FusionAuth java-http

repository·main·Indexed 43 days ago

https://github.com/fusionauth/java-http

A high-performance, dependency-free HTTP client and server implementation for Java. It leverages Project Loom virtual threads and blocking I/O to achieve high throughput and simplicity. Supports TLS 1.0-1.3 via Java SSLEngine. Current stable version is 1.4.0.

Tokens
4.4K
Snippets
9
Records
20
Agent score
56%

What's inside java-http

  1. Understand the benchmark tool options

    main

    The framework supports two primary benchmarking tools:

    1. wrk (Primary): A high-performance C-based HTTP benchmark tool using kqueue (macOS) or epoll (Linux). It is used for high-throughput testing and provides latency percentiles (p50, p90, p99) via Lua callbacks. Scenarios are defined in load-tests/scenarios/.

      • Installation: brew install wrk (macOS) or apt install wrk (Linux).
    2. fusionauth-load-tests (Secondary): A Java-based load generator using virtual threads and the JDK HttpClient. It provides a perspective from a 'real Java client'. It typically achieves lower RPS than wrk due to client-side overhead.

    Note: You can run both tools against a server using the --tool both flag in run-benchmarks.sh.

  2. How HTTPOutputStream handles data and compression

    main

    The HTTPOutputStream acts as the bridge between the Worker thread and the Server. It manages data buffering and optional compression.

    Compression Logic

    To support compressed responses, the HTTPOutputStream must inspect the HTTPRequest. If the request allows it via the Accept-Encoding header, the HTTPOutputStream is wrapped with a GzipOutputStream. This ensures that as the Worker thread writes bytes, they are compressed inline before being handed to the Server.

    Buffering and Chunking Mechanism

    The HTTPOutputStream uses a synchronized chunking mechanism to pass data to the Server:

    • Chunk Size: Data is managed using ByteBuffers of a fixed chunk size (e.g., 1024 bytes).
    • Transfer Process: When a chunk is full, it is moved from the 'write side' to the 'read side' and returned to the Server. If the chunk is not yet full, the method returns null.
    • Closing: When close() is called, any remaining bytes in the write buffer are immediately transferred to the read side so the Server can finish the transmission.
  3. Use different benchmark tools: wrk vs fusionauth-load-tests

    main

    The framework supports two distinct benchmarking approaches:

    wrk (Default)

    • Characteristics: C-based, uses epoll/kqueue. Extremely fast and unlikely to bottleneck the server.
    • Metrics: Provides percentile latency metrics (p50, p90, p99).
    • Best Use Case: Measuring the absolute throughput ceiling of a server.

    fusionauth-load-tests

    • Characteristics: Java-based, uses virtual threads and the JDK HttpClient.
    • Metrics: Provides average latency and RPS, but not percentiles.
    • Best Use Case: Simulating a realistic "Java application calling an HTTP server" workload.

    To use both tools in a single run, use --tool both.

  4. How HTTP requests are processed by the server

    main

    The server processes incoming HTTP requests in two distinct phases: the head section (start-line and headers) and the body section.

    1. Head Section Processing: The server reads bytes from the client and processes them sequentially until the end of the head section is reached. This phase uses a single ByteBuffer for efficiency, as it is managed by a single server thread and does not require thread safety. The request parser handles characters inline during this phase.
    2. Body Section Processing: Once the head section is complete, the server transitions into body piping mode. The remaining bytes in the buffer are handed off to a thread-safe pipe. A Worker thread then begins executing the request by reading from this pipe in a streaming fashion.

    To ensure high performance and non-blocking behavior, the pipe used for the body must block on the read side (the Worker thread) rather than the write side (the server thread). This allows the server thread to continue reading from the network while the Worker thread processes the stream.

  5. Understand the HTTP response lifecycle

    main

    The HTTP response process is split between a Worker thread (which handles application logic) and the Server (which handles network I/O). This separation allows the worker to focus on generating data while the server manages the transmission of headers and body chunks.

    The Lifecycle Steps:

    1. Header & Status Preparation: The Worker thread runs a handler that writes headers and the status code to the HTTPResponse. The Server monitors this state (e.g., via isHeadReady() or state()) without participating in the logic.
    2. Signaling Completion: The Worker thread signals it is finished by either calling close() on the OutputStream or by writing the first byte. This signal tells the Server to prepare the head section.
    3. Head Writing: The Server converts the HTTPResponse into bytes (the head section) and writes it to the client.
    4. Body Chunking & Compression: As the Worker thread writes bytes to the OutputStream, the Server reads these chunks and writes them to the client. If the client's HTTPRequest included an Accept-Encoding header that allows it, the Server performs inline compression (e.g., gzip).
  6. Update the project README performance table

    main

    After running benchmarks, use update-readme.sh to automatically parse the latest JSON results from load-tests/results/ and generate a markdown performance table. This script replaces the ## Performance section in the project's root README.md with the most recent data.

    ./update-readme.sh
  7. Compare benchmark results

    main

    Benchmark results are saved as JSON files in load-tests/results/ using ISO timestamps (e.g., results/YYYY-MM-DDTHH-MM-SSZ.json).

    To identify performance regressions or improvements, use the compare-results.sh script to compare two JSON files. This script calculates normalized ratios between the files.

    ./compare-results.sh results/2026-02-17T20-30-00Z-baseline.json results/2026-02-17T21-00-00Z-after-change.json
  8. Run the load testing and benchmarking suite

    main

    The run-benchmarks.sh script is the main orchestrator for the benchmarking framework. It automates the process of building servers via Savant, starting them, executing benchmarks using specified tools, stopping the servers, and aggregating the results into JSON files.

    To run the benchmarks, use the following command structure:

    ./run-benchmarks.sh [OPTIONS]

    Available Options

    OptionDescription
    --servers <list>Comma-separated list of servers to test (default: all)
    --scenarios <list>Comma-separated list of scenarios to run (default: all)
    --tool <name>Benchmark tool to use: wrk, fusionauth, or both (default: wrk)
    --label <name>A label to assign to the resulting JSON file
    --output <dir>Directory where results are saved (default: load-tests/results/)
    --duration <time>Duration for each scenario (default: 30s)
    ./run-benchmarks.sh [OPTIONS]
  9. Create a simple HTTP server

    main

    To create a server, implement an HTTPHandler to process requests and responses, then instantiate an HTTPServer. You must provide an HTTPListenerConfiguration specifying the port. The HTTPServer implements java.io.Closeable, so it is recommended to use it within a try-with-resources block to ensure proper shutdown.

    import io.fusionauth.http.server.HTTPListenerConfiguration;
    import io.fusionauth.http.server.HTTPServer;
    import io.fusionauth.http.server.HTTPHandler;
    
    public class Example {
      public static void main(String... args) {
        HTTPHandler handler = (req, res) -> {
          // Handler code goes here
        };
    
        try (HTTPServer server = new HTTPServer().withHandler(handler)
                                                 .withListener(new HTTPListenerConfiguration(4242))) {
          server.start();
          // When this block exits, the server will be shutdown
        }
      }
    }
  10. Build and start individual servers manually

    main

    Each server in the benchmark suite can be built independently using the Savant (sb) tool.

    Building: For most servers:

    cd load-tests/<server> && sb clean app

    For Tomcat (requires a specific target):

    cd load-tests/tomcat && sb clean tomcat

    Starting: After building, you can start a server manually using the generated distribution script:

    cd load-tests/<server>/build/dist && ./start.sh
  11. Enable TLS on the HTTP server

    main

    The server supports TLS 1.0-1.3 via the Java SSLEngine. To enable TLS, pass a certificate string and a private key string to the HTTPListenerConfiguration constructor.

    In development, you can use mkcert to generate self-signed certificates. In production, ensure your certificate file includes all intermediate CA certificates in the chain.

    import java.nio.file.Files;
    import java.nio.file.Paths;
    import io.fusionauth.http.server.HTTPHandler;
    import io.fusionauth.http.server.HTTPServer;
    import io.fusionauth.http.server.HTTPListenerConfiguration;
    
    public class Example {
      public static void main(String... args) throws Exception {
        String homeDir = System.getProperty("user.home");
        String certificate = Files.readString(Paths.get(homeDir + "/dev/certificates/example.org.pem"));
        String privateKey = Files.readString(Paths.get(homeDir + "/dev/certificates/example.org.key"));
    
        HTTPHandler handler = (req, res) -> {
          // Handler code goes here
        };
    
        try (HTTPServer server = new HTTPServer().withHandler(handler)
                                            .withListener(new HTTPListenerConfiguration(4242, certificate, privateKey))) {
          server.start();
        }
      }
    }