From cdecf531161639f2f9fe98b5a8cc3facd43bb116 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Thu, 01 Oct 2026 13:54:13 +0000
Subject: [PATCH] [#1143] Do not report a Notice of Disconnection that cannot reach an already-closed client as an unhandled error (#1146)

---
 opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/LDAPServerFilter.java          |   24 +++++++----
 opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/ConnectionFactoryTestCase.java |   62 +++++++++++++++++++++++++++++++
 2 files changed, 77 insertions(+), 9 deletions(-)

diff --git a/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/LDAPServerFilter.java b/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/LDAPServerFilter.java
index a1a5cc5..12ff173 100644
--- a/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/LDAPServerFilter.java
+++ b/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/LDAPServerFilter.java
@@ -82,10 +82,10 @@
 
 import com.forgerock.reactive.Action;
 import com.forgerock.reactive.Completable;
+import com.forgerock.reactive.Consumer;
 import com.forgerock.reactive.ReactiveHandler;
 import com.forgerock.reactive.Stream;
 
-import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
 import org.openidentityplatform.rxjava3.internal.util.BackpressureHelper;
 
 /**
@@ -542,7 +542,19 @@
                     // handleClose() will be invoked once this connection has been closed.
                     connection.closeSilently();
                 }
-            }).subscribe();
+            }).subscribe(new Action() {
+                @Override
+                public void run() throws Exception {
+                    // Nothing to do: the connection is closed on either outcome.
+                }
+            }, new Consumer<Throwable>() {
+                @Override
+                public void accept(final Throwable error) throws Exception {
+                    // Expected when the client has already closed its end: the notice cannot be delivered,
+                    // and the connection is closed anyway.
+                    logger.traceException(error);
+                }
+            });
         }
 
         private void notifyConnectionClosedRawUnbind(final LdapRequestEnvelope rawUnbindRequest) {
@@ -645,13 +657,7 @@
                     }).thenOnException(new ExceptionHandler<Exception>() {
                         @Override
                         public void handleException(Exception exception) {
-                            try {
-                            	 s.onError(exception);
-                            } catch (Throwable t) {
-                                if (!(t instanceof OnErrorNotImplementedException)) {
-                                    throw t;
-                                }
-                            }
+                            s.onError(exception);
                         }
                     }).thenOnRuntimeException(new RuntimeExceptionHandler() {
                         @Override
diff --git a/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/ConnectionFactoryTestCase.java b/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/ConnectionFactoryTestCase.java
index 87a652c..c5e5499 100644
--- a/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/ConnectionFactoryTestCase.java
+++ b/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/ConnectionFactoryTestCase.java
@@ -37,7 +37,9 @@
 import java.net.InetSocketAddress;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
 import java.util.concurrent.Callable;
+import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
@@ -95,6 +97,8 @@
 
 import com.forgerock.reactive.ServerConnectionFactoryAdapter;
 
+import io.reactivex.rxjava3.plugins.RxJavaPlugins;
+
 /**
  * Tests the {@code ConnectionFactory} classes.
  */
@@ -665,6 +669,64 @@
         }
     }
 
+    /**
+     * A Notice of Disconnection that cannot be written because the client has already closed its end is an
+     * expected outcome: it must not be reported as an unhandled error (issue #1143).
+     */
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testDisconnectWithNotificationToClosedClientIsNotReportedAsUnhandledError() throws Exception {
+        final CountDownLatch connectLatch = new CountDownLatch(1);
+        final AtomicReference<LDAPClientContext> contextHolder = new AtomicReference<>();
+        final ServerConnectionFactory<LDAPClientContext, Integer> mockServer =
+                mock(ServerConnectionFactory.class);
+        when(mockServer.handleAccept(any(LDAPClientContext.class))).thenAnswer(
+                new Answer<ServerConnection<Integer>>() {
+                    @Override
+                    public ServerConnection<Integer> answer(InvocationOnMock invocation) throws Throwable {
+                        contextHolder.set((LDAPClientContext) invocation.getArguments()[0]);
+                        connectLatch.countDown();
+                        return mock(ServerConnection.class);
+                    }
+                });
+
+        final List<Throwable> unhandledErrors = new CopyOnWriteArrayList<>();
+        final io.reactivex.rxjava3.functions.Consumer<? super Throwable> previousErrorHandler =
+                RxJavaPlugins.getErrorHandler();
+        RxJavaPlugins.setErrorHandler(new io.reactivex.rxjava3.functions.Consumer<Throwable>() {
+            @Override
+            public void accept(Throwable error) {
+                unhandledErrors.add(error);
+            }
+        });
+        LDAPListener listener = new LDAPListener(Collections.singleton(loopbackWithDynamicPort()),
+                new ServerConnectionFactoryAdapter(Options.defaultOptions().get(LDAP_DECODE_OPTIONS), mockServer));
+        try {
+            final InetSocketAddress listenerAddr = listener.getSocketAddresses().iterator().next();
+            final Connection client = new LDAPConnectionFactory(listenerAddr.getHostName(),
+                    listenerAddr.getPort()).getConnection();
+            assertThat(connectLatch.await(TEST_TIMEOUT, TimeUnit.SECONDS)).isTrue();
+            final LDAPClientContext context = contextHolder.get();
+
+            // The client leaves first: wait until the server has seen the connection close.
+            client.close();
+            waitForCondition(new Callable<Boolean>() {
+                @Override
+                public Boolean call() throws Exception {
+                    return context.isClosed();
+                }
+            });
+
+            // Writing the notice now fails, and does so before disconnect() returns.
+            context.disconnect(ResultCode.BUSY, "busy");
+
+            assertThat(unhandledErrors).isEmpty();
+        } finally {
+            RxJavaPlugins.setErrorHandler(previousErrorHandler);
+            listener.close();
+        }
+    }
+
     @Test(description = "Test for OPENDJ-1121: Closing a connection after "
             + "closing the connection factory causes NPE")
     public void testFactoryCloseBeforeConnectionClose() throws Exception {

--
Gitblit v1.10.0