Class AsyncResponseProcessor

java.lang.Object
org.apache.juneau.rest.server.processor.AsyncResponseProcessor
All Implemented Interfaces:
ResponseProcessor

public class AsyncResponseProcessor extends Object implements ResponseProcessor
Response processor that unwraps CompletableFuture / CompletionStage return values from @RestOp-annotated methods and bridges them to the servlet container's asynchronous request lifecycle.

When a handler returns a future, this processor is the FIRST processor in the default chain (see DefaultConfig). It detects the CompletionStage content, calls ServletRequest.startAsync() on the underlying servlet request, and registers a whenComplete callback that:

Synchronous fallback

In environments where ServletRequest.startAsync() is unsupported (most notably Juneau's MockServletRequest, which returns null), this processor falls back to a blocking get(timeout) on the future and then returns ResponseProcessor.RESTART so the rest of the chain runs synchronously on the unwrapped value. This keeps the unit-test surface working without requiring a real servlet container while preserving full AsyncContext semantics in production.

Timeout

The async timeout is configurable via @Rest(asyncTimeoutMillis) / @RestOp(asyncTimeoutMillis) (default 30s). On timeout the processor cancels the future with mayInterruptIfRunning=true and writes a HttpServletResponse.SC_GATEWAY_TIMEOUT response.

Bare Future rejection

Bare Future return types (anything that is a Future but not a CompletionStage) are rejected with an InternalServerError — polling a Future would block the request thread without any of the cancellation / cooperation guarantees that CompletableFuture provides. Handlers that need async dispatch must return CompletableFuture or CompletionStage.

Completion executor

When @Rest(asyncCompletionExecutor="poolName") or the per-op equivalent is set, this processor switches from future.whenComplete(callback) to future.whenCompleteAsync(callback, executor), routing the response-handler work through the named Executor bean. The MDC snapshot (see below) is taken on the request thread before the executor routing so the MDC context is always available regardless of which thread the executor picks.

Thread-local caveats

Anything carried via ThreadLocal (SLF4J MDC, security contexts) does NOT survive the async boundary — the whenComplete callback typically runs on a different thread than the one that produced the future. RequestAttributes, VarResolverSession, and Locale are request-scoped and survive correctly.

See Also:
Since:
10.0.0
  • Field Details

  • Constructor Details

  • Method Details

    • process

      public int process(RestOpSession opSession) throws IOException, BasicHttpException
      Description copied from interface: ResponseProcessor
      Process this response if possible.
      Specified by:
      process in interface ResponseProcessor
      Parameters:
      opSession - The HTTP call.
      Returns:
      One of the following codes:
      • 0 - The processor could not handle the request.
      • 1 - The processor was able to fully handle the request.
      • 2 - The processor was able to partially handle the request by replacing the output. The response processors should start over.
      Throws:
      IOException - If low-level exception occurred on output stream.
      Results in a HttpServletResponse.SC_INTERNAL_SERVER_ERROR error.
      BasicHttpException - If some other exception occurred.
      Can be used to provide an appropriate HTTP response code and message.
    • markAsyncOwnedAndSnapshot

      public static void markAsyncOwnedAndSnapshot(RestOpSession opSession)
      Publishes async ownership and the resolved debug snapshot on the request thread, before any completion callback or streaming subscription is registered.

      Shared by AsyncResponseProcessor and the reactive streaming processor so both async-owned families use the same race-safe handoff: the ownership flag makes RestSession.finish() skip synchronous flush/emit, and the snapshot carries the resolved logger/formatter/detail-tier across the async hop.

      Parameters:
      opSession - The operation session being handed off to async dispatch. Must not be null.
    • isAsyncDispatchOwned

      public static boolean isAsyncDispatchOwned(RestOpSession opSession)
      Returns whether the given session has been handed off to async dispatch. Inspected by RestOpSession.finish() / RestSession.finish() to skip the synchronous flushBuffer() call when the AsyncContext.complete() path will commit the response itself.
      Parameters:
      opSession - The session to check.
      Can be null (returns false).
      Returns:
      true if ATTR_ASYNC_DISPATCH_OWNED has been set.
    • isAsyncDispatchOwned

      public static boolean isAsyncDispatchOwned(jakarta.servlet.http.HttpServletRequest req)
      Variant of isAsyncDispatchOwned(RestOpSession) that operates directly on the underlying servlet request — used by RestSession.finish() where the RestOpSession may not be available.
      Parameters:
      req - The servlet request.
      Can be null (returns false).
      Returns:
      true if ATTR_ASYNC_DISPATCH_OWNED has been set.