This is an automated email from the ASF dual-hosted git repository.

Croway pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-spring-boot.git


The following commit(s) were added to refs/heads/main by this push:
     new 4901b33d6bc fix: do not overwrite async timeout response with 500 in 
afterProcess
4901b33d6bc is described below

commit 4901b33d6bc4e2697e637b7941a83015de39cc6f
Author: Croway <[email protected]>
AuthorDate: Mon May 4 19:19:17 2026 +0200

    fix: do not overwrite async timeout response with 500 in afterProcess
    
    When Spring's async timeout handler sends 503, the executor thread's
    afterProcess() catches AsyncRequestNotUsableException from writeResponse()
    and falls through to sendError(500), overwriting the 503. Skip the
    sendError(500) fallback when the failure is AsyncRequestNotUsableException.
---
 .../springboot/SpringBootPlatformHttpConsumer.java |  24 +++-
 .../SpringBootPlatformHttpAsyncNotUsableTest.java  | 151 +++++++++++++++++++++
 2 files changed, 171 insertions(+), 4 deletions(-)

diff --git 
a/components-starter/camel-platform-http-starter/src/main/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpConsumer.java
 
b/components-starter/camel-platform-http-starter/src/main/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpConsumer.java
index 2b86c7878d0..a92f7877e2c 100644
--- 
a/components-starter/camel-platform-http-starter/src/main/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpConsumer.java
+++ 
b/components-starter/camel-platform-http-starter/src/main/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpConsumer.java
@@ -98,7 +98,7 @@ public class SpringBootPlatformHttpConsumer extends 
DefaultConsumer implements P
                 // do not leak exception back to caller
                 LOG.warn("Error handling request due to: {}", e.getMessage(), 
e);
                 try {
-                    if (!response.isCommitted()) {
+                    if (!isAsyncRequestNotUsable(e) && 
!response.isCommitted()) {
                         
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                     }
                 } catch (Exception e1) {
@@ -149,6 +149,7 @@ public class SpringBootPlatformHttpConsumer extends 
DefaultConsumer implements P
 
     protected void afterProcess(HttpServletResponse response, Exchange 
exchange) throws Exception {
         boolean writeFailure = false;
+        boolean asyncCompleted = false;
         try {
             // now lets output to the res
             if (LOG.isTraceEnabled()) {
@@ -156,14 +157,19 @@ public class SpringBootPlatformHttpConsumer extends 
DefaultConsumer implements P
             }
             binding.writeResponse(exchange, response);
         } catch (Exception e) {
-            writeFailure = true;
-            handleFailure(exchange, e);
+            if (isAsyncRequestNotUsable(e)) {
+                asyncCompleted = true;
+                LOG.debug("Async request already completed, response not 
usable: {}", e.getMessage());
+            } else {
+                writeFailure = true;
+                handleFailure(exchange, e);
+            }
         } finally {
             doneUoW(exchange);
             releaseExchange(exchange, false);
         }
         try {
-            if (writeFailure && !response.isCommitted()) {
+            if (writeFailure && !asyncCompleted && !response.isCommitted()) {
                 
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
             }
         } catch (Exception e) {
@@ -172,6 +178,16 @@ public class SpringBootPlatformHttpConsumer extends 
DefaultConsumer implements P
 
     }
 
+    static boolean isAsyncRequestNotUsable(Throwable e) {
+        for (Throwable t = e; t != null; t = t.getCause()) {
+            if 
("org.springframework.web.context.request.async.AsyncRequestNotUsableException"
+                    .equals(t.getClass().getName())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     private void handleFailure(Exchange exchange, Throwable failure) {
         getExceptionHandler().handleException(
                 "Failed writing HTTP response url: " + getEndpoint().getPath() 
+ " due to: " + failure.getMessage(),
diff --git 
a/components-starter/camel-platform-http-starter/src/test/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpAsyncNotUsableTest.java
 
b/components-starter/camel-platform-http-starter/src/test/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpAsyncNotUsableTest.java
new file mode 100644
index 00000000000..74063f9c919
--- /dev/null
+++ 
b/components-starter/camel-platform-http-starter/src/test/java/org/apache/camel/component/platform/http/springboot/SpringBootPlatformHttpAsyncNotUsableTest.java
@@ -0,0 +1,151 @@
+/*
+ * 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.platform.http.springboot;
+
+import jakarta.servlet.http.HttpServletResponse;
+import org.apache.camel.Exchange;
+import org.apache.camel.Processor;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.platform.http.PlatformHttpEndpoint;
+import org.apache.camel.component.platform.http.spi.PlatformHttpConsumer;
+import org.apache.camel.component.platform.http.spi.PlatformHttpEngine;
+import org.apache.camel.http.common.DefaultHttpBinding;
+import org.apache.camel.spring.boot.CamelAutoConfiguration;
+import org.apache.camel.test.spring.junit6.CamelSpringBootTest;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import 
org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+import 
org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.test.web.servlet.client.EntityExchangeResult;
+import org.springframework.test.web.servlet.client.RestTestClient;
+import 
org.springframework.web.context.request.async.AsyncRequestNotUsableException;
+
+import java.io.IOException;
+
+/**
+ * Verifies that afterProcess() does NOT call sendError(500) when
+ * writeResponse() throws AsyncRequestNotUsableException.
+ *
+ * When Spring's async timeout fires, the timeout handler already sends 503.
+ * Camel's executor thread still runs and afterProcess() catches the write
+ * failure. The current (buggy) code calls sendError(500) which overwrites
+ * the 503. The fix should detect AsyncRequestNotUsableException and skip
+ * the sendError(500) fallback.
+ */
+@EnableAutoConfiguration
+@CamelSpringBootTest
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, 
classes = { CamelAutoConfiguration.class,
+        SpringBootPlatformHttpAsyncNotUsableTest.TestConfiguration.class,
+        PlatformHttpComponentAutoConfiguration.class, 
SpringBootPlatformHttpAutoConfiguration.class })
+@AutoConfigureRestTestClient
+public class SpringBootPlatformHttpAsyncNotUsableTest {
+
+    @Autowired
+    RestTestClient restTestClient;
+
+    @Test
+    public void testAsyncNotUsableShouldNotOverwriteWith500() throws Exception 
{
+        EntityExchangeResult<String> result = 
restTestClient.get().uri("/async-not-usable")
+                .exchange()
+                .expectBody(String.class)
+                .returnResult();
+
+        // When writeResponse() throws AsyncRequestNotUsableException,
+        // afterProcess() should NOT call sendError(500).
+        // In a real timeout scenario, Spring's timeout handler already sent 
503.
+        // Overwriting with 500 is the bug we're fixing.
+        Assertions.assertThat(result.getStatus().value()).isNotEqualTo(500);
+    }
+
+    @Test
+    public void testRegularIOExceptionStillReturns500() throws Exception {
+        EntityExchangeResult<String> result = 
restTestClient.get().uri("/io-error")
+                .exchange()
+                .expectBody(String.class)
+                .returnResult();
+
+        // Regular IOExceptions during writeResponse() should still produce 
500.
+        // This is the existing correct behavior — only 
AsyncRequestNotUsableException
+        // should be treated differently.
+        Assertions.assertThat(result.getStatus().value()).isEqualTo(500);
+    }
+
+    @Configuration
+    public static class TestConfiguration {
+        @Bean
+        public SecurityFilterChain securityFilterChain(HttpSecurity http) 
throws Exception {
+            http.authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
+                .csrf(csrf -> csrf.disable());
+            return http.build();
+        }
+
+        @Bean(name = "platform-http-engine")
+        public PlatformHttpEngine asyncNotUsableEngine(Environment env) {
+            int port = Integer.parseInt(env.getProperty("server.port", 
"8080"));
+            return new AsyncNotUsableEngine(port);
+        }
+
+        @Bean
+        public RouteBuilder servletPlatformHttpRouteBuilder() {
+            return new RouteBuilder() {
+                @Override
+                public void configure() {
+                    from("platform-http:/async-not-usable")
+                            .setBody().constant("ok");
+                    from("platform-http:/io-error")
+                            .setBody().constant("ok");
+                }
+            };
+        }
+    }
+
+    private static class AsyncNotUsableBinding extends DefaultHttpBinding {
+
+        @Override
+        public void writeResponse(Exchange exchange, HttpServletResponse 
response) throws IOException {
+            String uri = exchange.getMessage().getHeader(Exchange.HTTP_URI, 
String.class);
+            if ("/async-not-usable".equals(uri)) {
+                throw new AsyncRequestNotUsableException("Response not usable 
after async request completion");
+            } else if ("/io-error".equals(uri)) {
+                throw new IOException("Simulated IO error");
+            } else {
+                super.writeResponse(exchange, response);
+            }
+        }
+    }
+
+    private static class AsyncNotUsableEngine extends 
SpringBootPlatformHttpEngine {
+
+        public AsyncNotUsableEngine(int port) {
+            super(port);
+        }
+
+        @Override
+        public PlatformHttpConsumer createConsumer(PlatformHttpEndpoint 
endpoint, Processor processor) {
+            SpringBootPlatformHttpConsumer answer = new 
SpringBootPlatformHttpConsumer(endpoint, processor);
+            answer.setBinding(new AsyncNotUsableBinding());
+            return answer;
+        }
+    }
+}

Reply via email to