Class AsyncResponseProcessor
- All Implemented Interfaces:
ResponseProcessor
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:
- On success — installs the unwrapped value via
RestResponse.setContent(Object), re-runsRestContext.processResponse(RestOpSession)(this processor sees a non-future and falls through toSerializedPojoProcessoret al.), flushes the response, and callsAsyncContext.complete(). - On failure — runs the throwable through
RestContext.convertThrowable(Throwable)so the existing error pipeline (includingThrowableProcessorandProblemDetailsProcessor) handles it, then completes theAsyncContext.
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 Summary
FieldsModifier and TypeFieldDescriptionstatic final StringRequest attribute set by this processor when theAsyncContextpath is taken so downstream lifecycle code (specificallyRestOpSession.finish()andRestSession.finish()) can skip the synchronousflushBuffer()/req.close()— theAsyncContext.complete()call inside thewhenCompletecallback is responsible for committing the response.static final longDefault async timeout in milliseconds when no annotation override is supplied.Fields inherited from interface org.apache.juneau.rest.server.processor.ResponseProcessor
FINISHED, NEXT, RESTART -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionstatic booleanisAsyncDispatchOwned(jakarta.servlet.http.HttpServletRequest req) Variant ofisAsyncDispatchOwned(RestOpSession)that operates directly on the underlying servlet request — used byRestSession.finish()where theRestOpSessionmay not be available.static booleanisAsyncDispatchOwned(RestOpSession opSession) Returns whether the given session has been handed off to async dispatch.static voidmarkAsyncOwnedAndSnapshot(RestOpSession opSession) Publishes async ownership and the resolved debug snapshot on the request thread, before any completion callback or streaming subscription is registered.intprocess(RestOpSession opSession) Process this response if possible.
-
Field Details
-
ATTR_ASYNC_DISPATCH_OWNED
Request attribute set by this processor when theAsyncContextpath is taken so downstream lifecycle code (specificallyRestOpSession.finish()andRestSession.finish()) can skip the synchronousflushBuffer()/req.close()— theAsyncContext.complete()call inside thewhenCompletecallback is responsible for committing the response.- Since:
- 10.0.0
- See Also:
-
DEFAULT_ASYNC_TIMEOUT_MILLIS
Default async timeout in milliseconds when no annotation override is supplied.- See Also:
-
-
Constructor Details
-
AsyncResponseProcessor
public AsyncResponseProcessor()
-
-
Method Details
-
process
Description copied from interface:ResponseProcessorProcess this response if possible.- Specified by:
processin interfaceResponseProcessor- 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 aHttpServletResponse.SC_INTERNAL_SERVER_ERRORerror.BasicHttpException- If some other exception occurred.
Can be used to provide an appropriate HTTP response code and message.
-
markAsyncOwnedAndSnapshot
Publishes async ownership and the resolved debug snapshot on the request thread, before any completion callback or streaming subscription is registered.Shared by
AsyncResponseProcessorand the reactive streaming processor so both async-owned families use the same race-safe handoff: the ownership flag makesRestSession.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 benull .
-
isAsyncDispatchOwned
Returns whether the given session has been handed off to async dispatch. Inspected byRestOpSession.finish()/RestSession.finish()to skip the synchronousflushBuffer()call when theAsyncContext.complete()path will commit the response itself.- Parameters:
opSession- The session to check.
Can benull (returnsfalse ).- Returns:
trueifATTR_ASYNC_DISPATCH_OWNEDhas been set.
-
isAsyncDispatchOwned
Variant ofisAsyncDispatchOwned(RestOpSession)that operates directly on the underlying servlet request — used byRestSession.finish()where theRestOpSessionmay not be available.- Parameters:
req- The servlet request.
Can benull (returnsfalse ).- Returns:
trueifATTR_ASYNC_DISPATCH_OWNEDhas been set.
-