From 77923475b6d3bd78e9965cfbfb13f1542c228af1 Mon Sep 17 00:00:00 2001 From: Thomas Cornish Date: Mon, 26 Oct 2026 06:38:38 +0000 Subject: [PATCH 1/2] Return an empty event mask from a closed IOSessionImpl getEventMask read SelectionKey#interestOps without the closed-status check the event-mask setters have, so it threw CancelledKeyException once the session's key was cancelled. SSLIOSession calls it on every setEvent, so a capacity update racing the close of a Connection: close response threw from ReactiveDataConsumer's Subscription#request, and the reactive stream never reached onComplete. Generated-by: Claude Code (Claude Opus 5.5) --- .../org/apache/hc/core5/reactor/IOSessionImpl.java | 10 +++++++++- .../apache/hc/core5/reactor/TestIOSessionImpl.java | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOSessionImpl.java b/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOSessionImpl.java index 8622c50a7..20131f7fd 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOSessionImpl.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOSessionImpl.java @@ -171,7 +171,15 @@ public SocketAddress getRemoteAddress() { @Override public int getEventMask() { - return this.key.interestOps(); + lock.lock(); + try { + if (isStatusClosed()) { + return 0; + } + return this.key.interestOps(); + } finally { + lock.unlock(); + } } @Override diff --git a/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestIOSessionImpl.java b/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestIOSessionImpl.java index 92ed454ab..d92c14d6a 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestIOSessionImpl.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestIOSessionImpl.java @@ -246,6 +246,20 @@ void eventMaskHelpersWork() throws Exception { } } + @Test + @SuppressWarnings("resource") + void eventMaskIsEmptyOnceClosed() throws Exception { + try (Selector selector = Selector.open(); + SocketChannel channel = SocketChannel.open()) { + channel.configureBlocking(false); + final SelectionKey key = channel.register(selector, SelectionKey.OP_READ); + final IOSessionImpl session = new IOSessionImpl("t", key, channel, null); + session.close(CloseMode.IMMEDIATE); + + Assertions.assertEquals(0, session.getEventMask()); + } + } + @Test @SuppressWarnings("resource") void toStringIncludesStatus() throws Exception { From 9b9592b60a35b44976a0e02d4bc583ebf2bbf222 Mon Sep 17 00:00:00 2001 From: Thomas Cornish Date: Tue, 27 Oct 2026 19:58:28 +0000 Subject: [PATCH 2/2] Check the closed status in getEventMask without the session lock SSLIOSession already holds the session lock when it calls getEventMask, and InternalChannel closes the channel on a CancelledKeyException from the remaining window, so the getter does not need the lock. Generated-by: Claude Code (Claude Opus 5.5) --- .../org/apache/hc/core5/reactor/IOSessionImpl.java | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOSessionImpl.java b/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOSessionImpl.java index 20131f7fd..03de8716b 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOSessionImpl.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOSessionImpl.java @@ -171,15 +171,10 @@ public SocketAddress getRemoteAddress() { @Override public int getEventMask() { - lock.lock(); - try { - if (isStatusClosed()) { - return 0; - } - return this.key.interestOps(); - } finally { - lock.unlock(); + if (isStatusClosed()) { + return 0; } + return this.key.interestOps(); } @Override