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/test/java/org/forgerock/opendj/grizzly/ConnectionFactoryTestCase.java |   62 +++++++++++++++++++++++++++++++
 1 files changed, 62 insertions(+), 0 deletions(-)

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