This is an automated email from the ASF dual-hosted git repository. acosentino pushed a commit to branch ci-issue-CAMEL-23295 in repository https://gitbox.apache.org/repos/asf/camel.git
commit 3f2a1da139c81c8cd484b68c545356c91ac05ee6 Author: Andrea Cosentino <[email protected]> AuthorDate: Wed Apr 8 09:38:05 2026 +0200 CAMEL-23295: Fix resource leak and improve error handling in camel-splunk-hec producer - Add warning log when skipTlsVerify is enabled to alert operators - Replace RuntimeException with RuntimeCamelException for proper Camel error handling - Log error response body at DEBUG level for easier debugging - Consume response entity on success path to prevent potential connection leaks - Add unit tests for error handling behavior Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> Signed-off-by: Andrea Cosentino <[email protected]> --- .../component/splunkhec/SplunkHECProducer.java | 15 ++- .../component/splunkhec/SplunkHECProducerTest.java | 137 +++++++++++++++++++++ 2 files changed, 150 insertions(+), 2 deletions(-) diff --git a/components/camel-splunk-hec/src/main/java/org/apache/camel/component/splunkhec/SplunkHECProducer.java b/components/camel-splunk-hec/src/main/java/org/apache/camel/component/splunkhec/SplunkHECProducer.java index 09c6665ac9e0..0936d801f727 100644 --- a/components/camel-splunk-hec/src/main/java/org/apache/camel/component/splunkhec/SplunkHECProducer.java +++ b/components/camel-splunk-hec/src/main/java/org/apache/camel/component/splunkhec/SplunkHECProducer.java @@ -24,6 +24,7 @@ import java.util.Map; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.camel.Exchange; import org.apache.camel.Message; +import org.apache.camel.RuntimeCamelException; import org.apache.camel.support.DefaultProducer; import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; @@ -36,13 +37,17 @@ import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.config.RegistryBuilder; import org.apache.hc.core5.http.io.entity.EntityTemplate; +import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.http.message.StatusLine; import org.apache.hc.core5.ssl.SSLContextBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The Splunk HEC producer. */ public class SplunkHECProducer extends DefaultProducer { + private static final Logger LOG = LoggerFactory.getLogger(SplunkHECProducer.class); private static final ObjectMapper MAPPER = new ObjectMapper(); private final SplunkHECEndpoint endpoint; private CloseableHttpClient httpClient; @@ -59,6 +64,9 @@ public class SplunkHECProducer extends DefaultProducer { .setUserAgent("Camel Splunk HEC/" + getEndpoint().getCamelContext().getVersion()); PoolingHttpClientConnectionManager connManager; if (endpoint.getConfiguration().isSkipTlsVerify()) { + LOG.warn("Splunk HEC endpoint is configured with skipTlsVerify=true." + + " TLS certificate and hostname verification are disabled." + + " This should not be used in production environments."); SSLContextBuilder sslbuilder = new SSLContextBuilder(); sslbuilder.loadTrustMaterial(null, (chain, authType) -> true); SSLConnectionSocketFactory sslsf @@ -100,9 +108,12 @@ public class SplunkHECProducer extends DefaultProducer { if (response.getCode() != 200) { ByteArrayOutputStream output = new ByteArrayOutputStream(); response.getEntity().writeTo(output); - - throw new RuntimeException(new StatusLine(response) + "\n" + output.toString(StandardCharsets.UTF_8)); + String responseBody = output.toString(StandardCharsets.UTF_8); + LOG.debug("Splunk HEC error response (HTTP {}): {}", response.getCode(), responseBody); + throw new RuntimeCamelException( + "Splunk HEC request failed: " + new StatusLine(response) + "\n" + responseBody); } + EntityUtils.consume(response.getEntity()); return null; }); } diff --git a/components/camel-splunk-hec/src/test/java/org/apache/camel/component/splunkhec/SplunkHECProducerTest.java b/components/camel-splunk-hec/src/test/java/org/apache/camel/component/splunkhec/SplunkHECProducerTest.java new file mode 100644 index 000000000000..366c84be0c61 --- /dev/null +++ b/components/camel-splunk-hec/src/test/java/org/apache/camel/component/splunkhec/SplunkHECProducerTest.java @@ -0,0 +1,137 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.splunkhec; + +import java.io.OutputStream; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; + +import org.apache.camel.Exchange; +import org.apache.camel.RuntimeCamelException; +import org.apache.camel.impl.DefaultCamelContext; +import org.apache.camel.support.DefaultExchange; +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.io.HttpClientResponseHandler; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class SplunkHECProducerTest { + + private DefaultCamelContext camelContext; + private SplunkHECEndpoint endpoint; + private SplunkHECProducer producer; + + @BeforeEach + void setUp() throws Exception { + camelContext = new DefaultCamelContext(); + camelContext.start(); + + SplunkHECConfiguration config = new SplunkHECConfiguration(); + config.setToken("11111111-1111-1111-1111-111111111111"); + config.setHttps(false); + config.setSkipTlsVerify(false); + + SplunkHECComponent component = new SplunkHECComponent(); + component.setCamelContext(camelContext); + + endpoint = new SplunkHECEndpoint("splunk-hec:localhost:8088", component, config); + endpoint.setSplunkURL("localhost:8088"); + + producer = new SplunkHECProducer(endpoint); + } + + @Test + public void testProcessThrowsRuntimeCamelExceptionOnNon200Response() throws Exception { + CloseableHttpClient mockClient = createMockClient(400, "Bad Request", + "{\"text\":\"Invalid data format\",\"code\":6}"); + injectHttpClient(producer, mockClient); + + Exchange exchange = new DefaultExchange(camelContext); + exchange.getIn().setBody("test event"); + + RuntimeCamelException thrown = assertThrows(RuntimeCamelException.class, () -> producer.process(exchange)); + assertTrue(thrown.getMessage().contains("Splunk HEC request failed")); + assertTrue(thrown.getMessage().contains("Invalid data format")); + } + + @Test + public void testProcessThrowsRuntimeCamelExceptionOnServerError() throws Exception { + CloseableHttpClient mockClient = createMockClient(503, "Service Unavailable", + "{\"text\":\"Server is busy\",\"code\":9}"); + injectHttpClient(producer, mockClient); + + Exchange exchange = new DefaultExchange(camelContext); + exchange.getIn().setBody("test event"); + + RuntimeCamelException thrown = assertThrows(RuntimeCamelException.class, () -> producer.process(exchange)); + assertTrue(thrown.getMessage().contains("Splunk HEC request failed")); + assertTrue(thrown.getMessage().contains("Server is busy")); + } + + @Test + public void testExceptionTypeIsRuntimeCamelExceptionNotRuntimeException() throws Exception { + CloseableHttpClient mockClient = createMockClient(401, "Unauthorized", + "{\"text\":\"Invalid token\",\"code\":4}"); + injectHttpClient(producer, mockClient); + + Exchange exchange = new DefaultExchange(camelContext); + exchange.getIn().setBody("test event"); + + Exception thrown = assertThrows(Exception.class, () -> producer.process(exchange)); + assertEquals(RuntimeCamelException.class, thrown.getClass()); + } + + @SuppressWarnings("unchecked") + private CloseableHttpClient createMockClient(int statusCode, String reasonPhrase, String responseBody) + throws Exception { + CloseableHttpClient mockClient = mock(CloseableHttpClient.class); + when(mockClient.execute(any(HttpPost.class), any(HttpClientResponseHandler.class))) + .thenAnswer(invocation -> { + HttpClientResponseHandler<Object> handler = invocation.getArgument(1); + ClassicHttpResponse mockResponse = mock(ClassicHttpResponse.class); + when(mockResponse.getCode()).thenReturn(statusCode); + when(mockResponse.getReasonPhrase()).thenReturn(reasonPhrase); + when(mockResponse.getVersion()).thenReturn(org.apache.hc.core5.http.HttpVersion.HTTP_1_1); + HttpEntity entity = mock(HttpEntity.class); + doAnswer(inv -> { + OutputStream os = inv.getArgument(0); + os.write(responseBody.getBytes(StandardCharsets.UTF_8)); + return null; + }).when(entity).writeTo(any(OutputStream.class)); + when(mockResponse.getEntity()).thenReturn(entity); + return handler.handleResponse(mockResponse); + }); + return mockClient; + } + + private void injectHttpClient(SplunkHECProducer producer, CloseableHttpClient client) throws Exception { + Field httpClientField = SplunkHECProducer.class.getDeclaredField("httpClient"); + httpClientField.setAccessible(true); + httpClientField.set(producer, client); + } +}
