@@ -469,7 +469,8 @@ private CompletableFuture<ResponseEntity<?>> dispatchAsyncFlow(
469469 appName , method , path , query , headers , body );
470470 return VesperaBridge .dispatch (wireReq )
471471 .thenApplyAsync (
472- wireResp -> buildResponseEntityFromWire (wireResp , method ),
472+ wireResp -> buildCappedResponseEntityFromWire (
473+ wireResp , method , maxBufferedResponseBytes ),
473474 asyncResponseExecutor )
474475 // The async executor uses AbortPolicy (NOT CallerRunsPolicy):
475476 // under saturation the heavy wire response build must NOT run on
@@ -911,6 +912,25 @@ static ResponseEntity<?> buildResponseEntityFromWire(byte[] wire, String method)
911912 new WireBodyResource (wire , bodyOff , bytesToExpose ), httpHeaders , status );
912913 }
913914
915+ /**
916+ * Build the buffered {@code ASYNC} response entity, enforcing the
917+ * {@code vespera.bridge.max-buffered-response-bytes} cap FIRST — parity with
918+ * the {@code SYNC} path ({@link #dispatchSync} via
919+ * {@link #rejectOversizedBufferedResponse}). Without this a custom
920+ * {@link DispatchModeResolver} returning {@link DispatchMode#ASYNC} would
921+ * heap-buffer an arbitrarily large Rust response (retained through
922+ * {@link WireBodyResource} until Spring finishes writing it), defeating the
923+ * cap and risking OOM / GC pressure. Runs on the async response executor
924+ * (NOT the Tokio completion thread), so the cap check stays off the native
925+ * worker. Package-private + static so the cap wiring is unit-testable
926+ * without a live JNI dispatch.
927+ */
928+ static ResponseEntity <?> buildCappedResponseEntityFromWire (
929+ byte [] wire , String method , long maxBufferedResponseBytes ) {
930+ rejectOversizedBufferedResponse (wire , maxBufferedResponseBytes );
931+ return buildResponseEntityFromWire (wire , method );
932+ }
933+
914934 static final class BodyPermittingOutputStream extends OutputStream {
915935 private final OutputStream delegate ;
916936 private final String method ;
0 commit comments