From 0e6f44aead43f91bbab66cbfc976a7ad86018e04 Mon Sep 17 00:00:00 2001 From: Valentino Porta Date: Sat, 3 Oct 2026 19:22:12 +0200 Subject: [PATCH 1/3] [CachedOutputStream]- Using a snapshot of cbs, so the single cb may unregister itself from the cos --- .../java/org/apache/cxf/io/CachedOutputStream.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/apache/cxf/io/CachedOutputStream.java b/core/src/main/java/org/apache/cxf/io/CachedOutputStream.java index 200b0dfe248..c99a5797440 100644 --- a/core/src/main/java/org/apache/cxf/io/CachedOutputStream.java +++ b/core/src/main/java/org/apache/cxf/io/CachedOutputStream.java @@ -164,6 +164,13 @@ public List getCallbacks() { return callbacks == null ? null : Collections.unmodifiableList(callbacks); } + private List getCallbacksSnapshot() { + if (callbacks == null || callbacks.isEmpty()) { + return Collections.emptyList(); + } + return new ArrayList<>(callbacks); + } + /** * Perform any actions required on stream flush (freeze headers, reset * output stream ... etc.) @@ -208,7 +215,8 @@ public void lockOutputStream() throws IOException { currentStream.flush(); outputLocked = true; if (null != callbacks) { - for (CachedOutputStreamCallback cb : callbacks) { + // Using a snapshot so the cb may unregister itself from the cos + for (CachedOutputStreamCallback cb : getCallbacksSnapshot()) { cb.onClose(this); } } @@ -221,7 +229,8 @@ public void close() throws IOException { currentStream.flush(); outputLocked = true; if (null != callbacks) { - for (CachedOutputStreamCallback cb : callbacks) { + // Using a snapshot so the cb may unregister itself from the cos + for (CachedOutputStreamCallback cb : getCallbacksSnapshot()) { try { cb.onClose(this); } catch (final RuntimeException ex) { From b25169393091ba32fee073640d2c22d892c275fe Mon Sep 17 00:00:00 2001 From: Valentino Porta Date: Sat, 3 Oct 2026 19:23:04 +0200 Subject: [PATCH 2/3] [LoggingCallback]- Deregister the LoggingCallback after onClose() has been called --- .../cxf/ext/logging/LoggingOutInterceptor.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/LoggingOutInterceptor.java b/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/LoggingOutInterceptor.java index 11a39cd2cf8..3ad634df698 100644 --- a/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/LoggingOutInterceptor.java +++ b/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/LoggingOutInterceptor.java @@ -193,11 +193,28 @@ public LoggingCallback(final LogEventSender sender, final Message msg, final Out this.lim = limit == -1 ? Integer.MAX_VALUE : limit; } + private void deregisterFrom(CachedOutputStream cos){ + // ------------- + // CXF-9251 + // If cos has a tmp file (so 'threshold' was triggered), after the introduction of DelayedCachedOutputStreamCleaner, a reference of LoggingOutputStream + // is held in a queue list ( DelayQueue queue ) + // If something goes wrong while closing the LoggingOutputStream, this can be recall and log twice (or more) when trying to delete the orphan tmp file. + // Furthermore, the LoggingOutputStream that registered this callback holds a reference to this Object, so it remains in memory avoiding GC until the DelayedCachedOutputStreamCleaner does his job (default 30 minutes) + // ------------- + // Doing, instead, this we should be ok :) + cos.deregisterCallback(this); + } + public void onFlush(CachedOutputStream cos) { } public void onClose(CachedOutputStream cos) { + + // The callback has been called, so we release it. + // Avoid logging twice in certain scenarios. + deregisterFrom(cos); + final LogEvent event = eventMapper.map(message, sensitiveProtocolHeaderNames); if (shouldLogContent(event)) { copyPayload(cos, event); From e49787f34f90b15a16dc2f2751f32cd5502c7b8e Mon Sep 17 00:00:00 2001 From: Valentino Porta Date: Sat, 3 Oct 2026 19:23:43 +0200 Subject: [PATCH 3/3] add test to ensure that the LoggingCallback has been deregistered from callbacks --- .../ext/logging/AttributeMaskSensitiveHelperTest.java | 9 +++++++++ .../test/java/org/apache/cxf/ext/logging/FaultTest.java | 3 +++ .../apache/cxf/ext/logging/MaskSensitiveHelperTest.java | 9 +++++++++ .../java/org/apache/cxf/ext/logging/TransformTest.java | 3 +++ .../java/org/apache/cxf/ext/logging/TruncatedTest.java | 3 +++ 5 files changed, 27 insertions(+) diff --git a/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/AttributeMaskSensitiveHelperTest.java b/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/AttributeMaskSensitiveHelperTest.java index 925cc9b5152..8b94df916fe 100644 --- a/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/AttributeMaskSensitiveHelperTest.java +++ b/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/AttributeMaskSensitiveHelperTest.java @@ -181,6 +181,9 @@ public void shouldReplaceSensitiveDataOutWithAdd() throws IOException { LogEvent event = logEventSender.getLogEvent(); assertNotNull(event); assertEquals(maskedContent, event.getPayload()); + + // Assert that the LoggingCallback has been deregistered from callbacks + assertEquals(0, ((LoggingOutputStream) out).getCallbacks().size()); } @Test @@ -203,6 +206,9 @@ public void shouldReplaceSensitiveDataOutWithSet() throws IOException { LogEvent event = logEventSender.getLogEvent(); assertNotNull(event); assertEquals(maskedContent, event.getPayload()); + + // Assert that the LoggingCallback has been deregistered from callbacks + assertEquals(0, ((LoggingOutputStream) out).getCallbacks().size()); } @Test @@ -222,6 +228,9 @@ public void shouldNotReplaceSensitiveDataEmptyExpression() throws IOException { LogEvent event = logEventSender.getLogEvent(); assertNotNull(event); assertEquals(loggingContent, event.getPayload()); + + // Assert that the LoggingCallback has been deregistered from callbacks + assertEquals(0, ((LoggingOutputStream) out).getCallbacks().size()); } private Message prepareInMessage() { diff --git a/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/FaultTest.java b/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/FaultTest.java index dd99dcebfa6..c7e6cbf23b0 100644 --- a/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/FaultTest.java +++ b/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/FaultTest.java @@ -65,5 +65,8 @@ public void logOnceForFaultsOccurringAfterLoggingOutPhase() throws IOException { assertEquals(1, logEventSender.getLogEvents().size()); assertEquals("TestMessage", logEventSender.getLogEvents().get(0).getPayload()); + + // Assert that the LoggingCallback has been deregistered from callbacks + assertEquals(0, ((LoggingOutputStream) postFaultOut).getCallbacks().size()); } } diff --git a/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/MaskSensitiveHelperTest.java b/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/MaskSensitiveHelperTest.java index 761409b3a6a..f3fef61d373 100644 --- a/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/MaskSensitiveHelperTest.java +++ b/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/MaskSensitiveHelperTest.java @@ -203,6 +203,9 @@ public void shouldReplaceSensitiveDataOutWithAdd() throws IOException { LogEvent event = logEventSender.getLogEvent(); assertNotNull(event); assertEquals(maskedContent, event.getPayload()); + + // Assert that the LoggingCallback has been deregistered from callbacks + assertEquals(0, ((LoggingOutputStream) out).getCallbacks().size()); } @Test @@ -224,6 +227,9 @@ public void shouldReplaceSensitiveDataOutWithSet() throws IOException { LogEvent event = logEventSender.getLogEvent(); assertNotNull(event); assertEquals(maskedContent, event.getPayload()); + + // Assert that the LoggingCallback has been deregistered from callbacks + assertEquals(0, ((LoggingOutputStream) out).getCallbacks().size()); } @Test @@ -243,6 +249,9 @@ public void shouldNotReplaceSensitiveDataEmptyExpression() throws IOException { LogEvent event = logEventSender.getLogEvent(); assertNotNull(event); assertEquals(loggingContent, event.getPayload()); + + // Assert that the LoggingCallback has been deregistered from callbacks + assertEquals(0, ((LoggingOutputStream) out).getCallbacks().size()); } private Message prepareInMessage() { diff --git a/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/TransformTest.java b/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/TransformTest.java index 48d53b0274f..b4ecf8e9ca9 100644 --- a/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/TransformTest.java +++ b/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/TransformTest.java @@ -92,6 +92,9 @@ public void transformOutboundInterceptorOutputStream() throws IOException { LogEvent event = logEventSender.getLogEvent(); assertNotNull(event); assertEquals(TRANSFORMED_LOGGING_CONTENT, event.getPayload()); + + // Assert that the LoggingCallback has been deregistered from callbacks + assertEquals(0, ((LoggingOutputStream) out).getCallbacks().size()); } @Test diff --git a/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/TruncatedTest.java b/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/TruncatedTest.java index a7567fc15fb..3e8045bbb95 100644 --- a/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/TruncatedTest.java +++ b/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/TruncatedTest.java @@ -68,6 +68,9 @@ public void truncatedOutboundInterceptorOutputStream() throws IOException { assertNotNull(event); assertEquals("T", event.getPayload()); // only the first byte is read! assertTrue(event.isTruncated()); + + // Assert that the LoggingCallback has been deregistered from callbacks + assertEquals(0, ((LoggingOutputStream) out).getCallbacks().size()); } @Test