Soklet Logo

Core Concepts

Request Lifecycle

Soklet provides two lifecycle hooks:

They're useful for tasks like:

  • Logging requests and responses
  • Performing authentication and authorization
  • Modifying requests/responses before downstream processing occurs
  • Wrapping downstream code in a database transaction
  • Monitoring unexpected errors that occur during internal processing (see Event Logging)

This is similar to the Jakarta EE Servlet Filter concept, but provides additional functionality beyond "wrap the whole request".


Request Interceptor

Request Wrapping

Wraps around the whole "outside" of an entire request-handling flow. The "inside" of the flow is everything from Resource Method execution to writing response bytes to the client. For a more fine-grained approach, see Request Intercepting.

Wrapping happens before Soklet resolves which Resource Method should handle the request. This means you can rewrite the HTTP method or path by returning a modified request via the consumer and Soklet will route using the wrapped request. You must call requestProcessor.accept(...) exactly once before returning; otherwise Soklet logs an error and returns a 500 response.

Wrapping a request is useful when you'd like to store off request-scoped information that needs to be made easily accessible to other parts of the system - for example, a request's Locale and ZoneId.

To implement, a useful scoping mechanism is Java's ScopedValue<T> (Java 25+) or ThreadLocal<T>. The former is demonstrated below.

// Special scoped value so anyone can access the current Locale.
// For Java < 25, use ThreadLocal instead
public static final ScopedValue<Locale> CURRENT_LOCALE;

// Spin up the ScopedValue (or ThreadLocal)
static {
  CURRENT_LOCALE = ScopedValue.newInstance();
}

SokletConfig config = SokletConfig.withServer(
  Server.fromPort(8080)
).requestInterceptor(new RequestInterceptor() {
  @Override
  public void wrapRequest(
    @NonNull ServerType serverType,
    @NonNull Request request,
    @NonNull Consumer<Request> requestProcessor
  ) {
    // Make the locale accessible by other code during this request...
    Locale locale = request.getLocales().get(0);
    
    // ...by binding it to a ScopedValue (or ThreadLocal).
    ScopedValue.where(CURRENT_LOCALE, locale).run(() -> {
      // You must call this so downstream processing can proceed
      requestProcessor.accept(request);
    });
  }
}).build();

Then, elsewhere in your code while a request is being processed:

class ExampleService {
  void accessCurrentLocale() {
    // You now have access to the Locale bound to the logical scope
    // (or Thread) without having to pass it down the call stack
    Locale locale = CURRENT_LOCALE.orElse(Locale.getDefault());
  }
}

References:

Request Intercepting

Conceptually, when a request comes in, Soklet will:

  1. Invoke the appropriate Resource Method to acquire a response
  2. Send the response over the wire to the client

Request Intercepting provides programmatic control over those two steps.

Example use cases are:

  • Wrapping Resource Methods with a database transaction (e.g. to ensure atomicity)
  • Customizing responses prior to sending over the wire

For a more coarse-grained approach, see Request Wrapping.

You must call responseWriter.accept(...) exactly once before returning; otherwise Soklet logs an error and returns a 500 response.

SokletConfig config = SokletConfig.withServer(
  Server.fromPort(8080)
).requestInterceptor(new RequestInterceptor() {
  @Override
  public void interceptRequest(
    @NonNull ServerType serverType,
    @NonNull Request request,
    @Nullable ResourceMethod resourceMethod,
    @NonNull Function<Request, MarshaledResponse> responseGenerator,
    @NonNull Consumer<MarshaledResponse> responseWriter
  ) {
    // Here's where you might start a DB transaction
    MyDatabase.INSTANCE.beginTransaction();

    // Step 1: Invoke the Resource Method and acquire its response
    MarshaledResponse response = responseGenerator.apply(request);

    // Commit the DB transaction before marshaling/sending the response
    // to reduce contention by keeping "open" time short
    MyDatabase.INSTANCE.commitTransaction();

    // You might also perform systemwide response adjustments here.
    // For example, set a special header via mutable copy
    response = response.copy().headers((mutableHeaders) -> {
      mutableHeaders.put("X-Powered-By", Set.of("Soklet"));
    }).finish();

    // Step 2: Send the finalized response over the wire
    responseWriter.accept(response);
  }
}).build();

References:

Lifecycle Observer

Soklet Start/Stop

Execute code immediately before and after a Soklet instance starts up and shuts down.

SokletConfig config = SokletConfig.withServer(
  Server.fromPort(8080)
).lifecycleObserver(new LifecycleObserver() {
  @Override
  public void willStartSoklet(@NonNull Soklet soklet) {
    // Perform startup tasks required prior to Soklet launch
    MyPayrollSystem.INSTANCE.startLengthyWarmupProcess();
  }

  @Override
  public void didStartSoklet(@NonNull Soklet soklet) {
    // Soklet has fully started.
    // Server (and optionally SSE server) are running
    System.out.println("Soklet started.");
  }

  @Override
  public void didFailToStartSoklet(
    @NonNull Soklet soklet,
    @NonNull Throwable throwable
  ) {
    System.err.println("Soklet failed to start.");
    throwable.printStackTrace();
  }

  @Override
  public void willStopSoklet(@NonNull Soklet soklet) {
    // Perform shutdown tasks required prior to Soklet teardown
    MyPayrollSystem.INSTANCE.destroy();
  }

  @Override
  public void didStopSoklet(@NonNull Soklet soklet) {
    // Soklet has fully shut down
    System.out.println("Soklet stopped.");
  }

  @Override
  public void didFailToStopSoklet(
    @NonNull Soklet soklet,
    @NonNull Throwable throwable
  ) {
    System.err.println("Soklet failed to stop cleanly.");
    throwable.printStackTrace();
  }
}).build();

References:

Server Start/Stop

Execute code immediately before and after the Soklet-managed Server starts up and shuts down.

SokletConfig config = SokletConfig.withServer(
  Server.fromPort(8080)
).lifecycleObserver(new LifecycleObserver() {
  @Override
  public void willStartServer(@NonNull Server server) {
    // Perform startup tasks required prior to server launch
    MyPayrollSystem.INSTANCE.startLengthyWarmupProcess();
  }

  @Override
  public void didStartServer(@NonNull Server server) {
    // Server has fully started up and is listening
    System.out.println("Server started.");
  }

  @Override
  public void didFailToStartServer(
    @NonNull Server server,
    @NonNull Throwable throwable
  ) {
    System.err.println("Server failed to start.");
    throwable.printStackTrace();
  }

  @Override
  public void willStopServer(@NonNull Server server) {
    // Perform shutdown tasks required prior to server teardown
    MyPayrollSystem.INSTANCE.destroy();    
  }

  @Override
  public void didStopServer(@NonNull Server server) {
    // Server has fully shut down
    System.out.println("Server stopped.");
  }

  @Override
  public void didFailToStopServer(
    @NonNull Server server,
    @NonNull Throwable throwable
  ) {
    System.err.println("Server failed to stop cleanly.");
    throwable.printStackTrace();
  }
}).build();

References:

Connection Acceptance

These methods fire when Soklet is about to accept a TCP connection, after it accepts one, or when it fails to accept a connection attempt. Each callback provides a ServerType, a best-effort remote address, and (for failures) a ConnectionRejectionReason plus an optional exception.

SokletConfig config = SokletConfig.withServer(
  Server.fromPort(8080)
).lifecycleObserver(new LifecycleObserver() {
  @Override
  public void willAcceptConnection(
    @NonNull ServerType serverType,
    @Nullable InetSocketAddress remoteAddress
  ) {
    System.out.printf("Incoming %s connection from %s\n", serverType, remoteAddress);
  }

  @Override
  public void didAcceptConnection(
    @NonNull ServerType serverType,
    @Nullable InetSocketAddress remoteAddress
  ) {
    System.out.printf("Accepted %s connection from %s\n", serverType, remoteAddress);
  }

  @Override
  public void didFailToAcceptConnection(
    @NonNull ServerType serverType,
    @Nullable InetSocketAddress remoteAddress,
    @NonNull ConnectionRejectionReason reason,
    @Nullable Throwable throwable
  ) {
    System.out.printf("Failed to accept %s connection from %s (%s)\n", serverType, remoteAddress, reason);
  }
}).build();

References:

Request Acceptance

These hooks fire as Soklet accepts requests for application-level handling and reads/parses them into Request instances.

SokletConfig config = SokletConfig.withServer(
  Server.fromPort(8080)
).lifecycleObserver(new LifecycleObserver() {
  @Override
  public void willAcceptRequest(
    @NonNull ServerType serverType,
    @Nullable InetSocketAddress remoteAddress,
    @Nullable String requestTarget
  ) {
    System.out.printf("About to accept request: %s%n", requestTarget);
  }

  @Override
  public void didAcceptRequest(
    @NonNull ServerType serverType,
    @Nullable InetSocketAddress remoteAddress,
    @Nullable String requestTarget
  ) {
    System.out.printf("Accepted request: %s%n", requestTarget);
  }

  @Override
  public void didFailToAcceptRequest(
    @NonNull ServerType serverType,
    @Nullable InetSocketAddress remoteAddress,
    @Nullable String requestTarget,
    @NonNull RequestRejectionReason reason,
    @Nullable Throwable throwable
  ) {
    System.err.printf("Failed to accept request (%s): %s%n", reason, requestTarget);
  }

  @Override
  public void willReadRequest(
    @NonNull ServerType serverType,
    @Nullable InetSocketAddress remoteAddress,
    @Nullable String requestTarget
  ) {
    System.out.printf("About to read request: %s%n", requestTarget);
  }

  @Override
  public void didReadRequest(
    @NonNull ServerType serverType,
    @Nullable InetSocketAddress remoteAddress,
    @Nullable String requestTarget
  ) {
    System.out.printf("Read request: %s%n", requestTarget);
  }

  @Override
  public void didFailToReadRequest(
    @NonNull ServerType serverType,
    @Nullable InetSocketAddress remoteAddress,
    @Nullable String requestTarget,
    @NonNull RequestReadFailureReason reason,
    @Nullable Throwable throwable
  ) {
    System.err.printf("Failed to read request (%s): %s%n", reason, requestTarget);
  }
}).build();

References:

Request Handling

These methods are fired at the very start of request processing and the very end, respectively.

SokletConfig config = SokletConfig.withServer(
  Server.fromPort(8080)
).lifecycleObserver(new LifecycleObserver() {
  @Override
  public void didStartRequestHandling(
    @NonNull ServerType serverType,
    @NonNull Request request,
    @Nullable ResourceMethod resourceMethod
  ) {
    System.out.printf("Received request: %s\n", request);

    // If there was no resourceMethod matching the request, expect a 404
    if(resourceMethod != null)
      System.out.printf("Request to be handled by: %s\n", resourceMethod);
    else
      System.out.println("This will be a 404.");
  }

  @Override
  public void didFinishRequestHandling(
    @NonNull ServerType serverType,
    @NonNull Request request,
    @Nullable ResourceMethod resourceMethod,
    @NonNull MarshaledResponse marshaledResponse,
    @NonNull Duration processingDuration,
    @NonNull List<Throwable> throwables
  ) {
    // We have access to a few things here...
    // * marshaledResponse is what was ultimately sent
    //    over the wire
    // * processingDuration is how long everything took, 
    //    including sending the response to the client
    // * throwables is the ordered list of exceptions
    //    thrown during execution (if any)
    double millis = processingDuration.toNanos() / 1_000_000.0;
    System.out.printf("Entire request took %dms\n", millis);
  }
}).build();

References:

Response Writing

Monitor the response writing process - sending bytes over the wire. You can respond to successful writes or failed writes (e.g. unexpected client disconnect).

SokletConfig config = SokletConfig.withServer(
  Server.fromPort(8080)
).lifecycleObserver(new LifecycleObserver() {
  @Override
  public void willWriteResponse(
    @NonNull ServerType serverType,
    @NonNull Request request,
    @Nullable ResourceMethod resourceMethod,
    @NonNull MarshaledResponse marshaledResponse
  ) {
    // Access to marshaledResponse here lets us see exactly
    // what will be going over the wire
    byte[] body = marshaledResponse.getBody().orElse(new byte[] {});
    System.out.printf("About to start writing response with " + 
      "a %d-byte body...\n", body.length);
  }

  @Override
  public void didWriteResponse(
    @NonNull ServerType serverType,
    @NonNull Request request,
    @Nullable ResourceMethod resourceMethod,
    @NonNull MarshaledResponse marshaledResponse,
    @NonNull Duration responseWriteDuration
  ) {
    double millis = responseWriteDuration.toNanos() / 1_000_000.0;
    System.out.printf("Took %dms to write response\n", millis);
  }

  @Override
  public void didFailToWriteResponse(
    @NonNull ServerType serverType,
    @NonNull Request request,
    @Nullable ResourceMethod resourceMethod,
    @NonNull MarshaledResponse marshaledResponse,
    @NonNull Duration responseWriteDuration,
    @NonNull Throwable throwable
  ) {
    double millis = responseWriteDuration.toNanos() / 1_000_000.0;
    System.out.printf("Response write failed after %dms\n", millis);

    // Use this to monitor trends in unexpected client disconnects
    System.err.println("Exception occurred while writing response");
    throwable.printStackTrace();
  }
}).build();

References:

Server-Sent Events

If your application configures a ServerSentEventServer, additional lifecycle hooks are available for SSE server and connection activity.

SSE Server Start/Stop

SokletConfig config = SokletConfig.withServer(
  Server.fromPort(8080)
).serverSentEventServer(
  ServerSentEventServer.fromPort(8081)
).lifecycleObserver(new LifecycleObserver() {
  @Override
  public void willStartServerSentEventServer(
    @NonNull ServerSentEventServer serverSentEventServer
  ) {
    System.out.println("SSE server starting.");
  }

  @Override
  public void didStartServerSentEventServer(
    @NonNull ServerSentEventServer serverSentEventServer
  ) {
    System.out.println("SSE server started.");
  }

  @Override
  public void didFailToStartServerSentEventServer(
    @NonNull ServerSentEventServer serverSentEventServer,
    @NonNull Throwable throwable
  ) {
    System.err.println("SSE server failed to start.");
    throwable.printStackTrace();
  }

  @Override
  public void willStopServerSentEventServer(
    @NonNull ServerSentEventServer serverSentEventServer
  ) {
    System.out.println("SSE server stopping.");
  }

  @Override
  public void didStopServerSentEventServer(
    @NonNull ServerSentEventServer serverSentEventServer
  ) {
    System.out.println("SSE server stopped.");
  }

  @Override
  public void didFailToStopServerSentEventServer(
    @NonNull ServerSentEventServer serverSentEventServer,
    @NonNull Throwable throwable
  ) {
    System.err.println("SSE server failed to stop cleanly.");
    throwable.printStackTrace();
  }
}).build();

References:

SSE Connections

SokletConfig config = SokletConfig.withServer(
  Server.fromPort(8080)
).serverSentEventServer(
  ServerSentEventServer.fromPort(8081)
).lifecycleObserver(new LifecycleObserver() {
  @Override
  public void willEstablishServerSentEventConnection(
    @NonNull Request request,
    @NonNull ResourceMethod resourceMethod
  ) {
    System.out.printf("Establishing SSE connection for %s\n", request);
  }

  @Override
  public void didEstablishServerSentEventConnection(
    @NonNull ServerSentEventConnection serverSentEventConnection
  ) {
    System.out.printf("SSE connection established: %s\n", serverSentEventConnection);
  }

  @Override
  public void didFailToEstablishServerSentEventConnection(
    @NonNull Request request,
    @Nullable ResourceMethod resourceMethod,
    @NonNull ServerSentEventConnection.HandshakeFailureReason reason,
    @Nullable Throwable throwable
  ) {
    System.err.println("SSE connection establishment failed.");
    if (throwable != null)
      throwable.printStackTrace();
  }

  @Override
  public void willTerminateServerSentEventConnection(
    @NonNull ServerSentEventConnection serverSentEventConnection,
    @NonNull TerminationReason terminationReason,
    @Nullable Throwable throwable
  ) {
    System.out.printf("SSE connection terminating (%s)\n", terminationReason);
  }

  @Override
  public void didTerminateServerSentEventConnection(
    @NonNull ServerSentEventConnection serverSentEventConnection,
    @NonNull Duration connectionDuration,
    @NonNull TerminationReason terminationReason,
    @Nullable Throwable throwable
  ) {
    System.out.printf("SSE connection terminated after %dms (%s)\n",
      connectionDuration.toMillis(), terminationReason);
  }
}).build();

References:

SSE Event and Comment Writes

SokletConfig config = SokletConfig.withServer(
  Server.fromPort(8080)
).serverSentEventServer(
  ServerSentEventServer.fromPort(8081)
).lifecycleObserver(new LifecycleObserver() {
  @Override
  public void willWriteServerSentEvent(
    @NonNull ServerSentEventConnection serverSentEventConnection,
    @NonNull ServerSentEvent serverSentEvent
  ) {
    System.out.printf("Writing SSE event %s\n", serverSentEvent.getEvent().orElse("message"));
  }

  @Override
  public void didWriteServerSentEvent(
    @NonNull ServerSentEventConnection serverSentEventConnection,
    @NonNull ServerSentEvent serverSentEvent,
    @NonNull Duration writeDuration
  ) {
    System.out.printf("SSE event write took %dms\n", writeDuration.toMillis());
  }

  @Override
  public void didFailToWriteServerSentEvent(
    @NonNull ServerSentEventConnection serverSentEventConnection,
    @NonNull ServerSentEvent serverSentEvent,
    @NonNull Duration writeDuration,
    @NonNull Throwable throwable
  ) {
    System.err.println("SSE event write failed.");
    throwable.printStackTrace();
  }

  @Override
  public void willWriteServerSentEventComment(
    @NonNull ServerSentEventConnection serverSentEventConnection,
    @NonNull ServerSentEventComment serverSentEventComment
  ) {
    System.out.printf("Writing SSE comment (%s): %s\n",
      serverSentEventComment.getCommentType(),
      serverSentEventComment.getComment().orElse("<heartbeat>"));
  }

  @Override
  public void didWriteServerSentEventComment(
    @NonNull ServerSentEventConnection serverSentEventConnection,
    @NonNull ServerSentEventComment serverSentEventComment,
    @NonNull Duration writeDuration
  ) {
    System.out.printf("SSE comment write took %dms\n", writeDuration.toMillis());
  }

  @Override
  public void didFailToWriteServerSentEventComment(
    @NonNull ServerSentEventConnection serverSentEventConnection,
    @NonNull ServerSentEventComment serverSentEventComment,
    @NonNull Duration writeDuration,
    @NonNull Throwable throwable
  ) {
    System.err.println("SSE comment write failed.");
    throwable.printStackTrace();
  }
}).build();

References:

Event Logging

Soklet provides insight into unexpected errors that occur during internal processing that are not otherwise surfaced via LogEvent objects provided to LifecycleObserver::didReceiveLogEvent.

For example, you might want to specially monitor scenarios in which your ResponseMarshaler::forThrowable failed (that is, you attempted to write an error response for an exception that bubbled out, but that attempt threw an exception, forcing Soklet to write its own failsafe response) - you could do this by observing events with type LogEventType.RESPONSE_MARSHALER_FOR_THROWABLE_FAILED.

Your LifecycleObserver can listen for LogEvents like this:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

SokletConfig config = SokletConfig.withServer(
  Server.fromPort(8080)
).lifecycleObserver(new LifecycleObserver() {
  // This example uses SLF4J. See https://www.slf4j.org
  private final Logger logger = 
    LoggerFactory.getLogger("com.soklet.example.LifecycleObserver");

  @Override
  public void didReceiveLogEvent(@NonNull LogEvent logEvent) {
    // These properties are available in LogEvent
    LogEventType logEventType = logEvent.getLogEventType();
    String message = logEvent.getMessage();
    Optional<Throwable> throwable = logEvent.getThrowable();    
    Optional<Request> request = logEvent.getRequest();
    Optional<ResourceMethod> resourceMethod = logEvent.getResourceMethod();
    Optional<MarshaledResponse> marshaledResponse = logEvent.getMarshaledResponse();

    // Log the message however you like
    logger.warn(message, throwable.orElse(null));
  }
}).build();

References:

Previous
Response Writing