From ee51e9ff656ce4787950f60487371de29c108079 Mon Sep 17 00:00:00 2001 From: akenra <37288280+akenra@users.noreply.github.com> Date: Thu, 30 Jul 2026 02:52:39 +0500 Subject: [PATCH] fix(aws): align Custom Runtime error format with Lambda Runtime Change errorType to fully qualified class name and stackTrace to JSON array of strings to match the standard Lambda Runtime format. Also add the Lambda-Runtime-Function-Error-Type header. Fixes gh-1428 Signed-off-by: akenra <37288280+akenra@users.noreply.github.com> --- .../adapter/aws/CustomRuntimeEventLoop.java | 19 +++-- .../adapter/test/aws/AWSCustomRuntime.java | 8 ++ .../aws/CustomRuntimeEventLoopTest.java | 73 +++++++++++++++++++ 3 files changed, 92 insertions(+), 8 deletions(-) diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/CustomRuntimeEventLoop.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/CustomRuntimeEventLoop.java index 7a63075b7..6a78a8fef 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/CustomRuntimeEventLoop.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/CustomRuntimeEventLoop.java @@ -17,16 +17,17 @@ package org.springframework.cloud.function.adapter.aws; import java.io.ByteArrayInputStream; -import java.io.PrintWriter; -import java.io.StringWriter; import java.net.SocketException; import java.net.URI; import java.nio.charset.StandardCharsets; import java.text.MessageFormat; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.stream.Collectors; import com.amazonaws.services.lambda.runtime.ClientContext; import com.amazonaws.services.lambda.runtime.CognitoIdentity; @@ -72,6 +73,8 @@ public final class CustomRuntimeEventLoop implements SmartLifecycle { private static final String LAMBDA_ERROR_URL_TEMPLATE = "http://{0}/{1}/runtime/invocation/{2}/error"; private static final String LAMBDA_RUNTIME_URL_TEMPLATE = "http://{0}/{1}/runtime/invocation/next"; private static final String LAMBDA_INVOCATION_URL_TEMPLATE = "http://{0}/{1}/runtime/invocation/{2}/response"; + private static final String LAMBDA_RUNTIME_FUNCTION_ERROR_TYPE = "Lambda-Runtime-Function-Error-Type"; + private static final String USER_AGENT_VALUE = String.format( "spring-cloud-function/%s-%s", System.getProperty("java.runtime.version"), @@ -282,12 +285,11 @@ public String toString() { private void propagateAwsError(String requestId, Exception e, JsonMapper mapper, String runtimeApi, RestTemplate rest) { String errorMessage = e.getMessage(); - String errorType = e.getClass().getSimpleName(); - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - e.printStackTrace(pw); - String stackTrace = sw.toString(); - Map em = new HashMap<>(); + String errorType = e.getClass().getName(); + List stackTrace = Arrays.stream(e.getStackTrace()) + .map(StackTraceElement::toString) + .collect(Collectors.toList()); + Map em = new HashMap<>(); em.put("errorMessage", errorMessage); em.put("errorType", errorType); em.put("stackTrace", stackTrace); @@ -296,6 +298,7 @@ private void propagateAwsError(String requestId, Exception e, JsonMapper mapper, String errorUrl = MessageFormat.format(LAMBDA_ERROR_URL_TEMPLATE, runtimeApi, LAMBDA_VERSION_DATE, requestId); ResponseEntity result = rest.exchange(RequestEntity.post(URI.create(errorUrl)) .header(USER_AGENT, USER_AGENT_VALUE) + .header(LAMBDA_RUNTIME_FUNCTION_ERROR_TYPE, errorType) .body(outputBody), Object.class); if (logger.isInfoEnabled()) { logger.info("Result ERROR status: " + result.getStatusCode()); diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/test/aws/AWSCustomRuntime.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/test/aws/AWSCustomRuntime.java index 28e303884..c6d7d947a 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/test/aws/AWSCustomRuntime.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/test/aws/AWSCustomRuntime.java @@ -44,6 +44,9 @@ public class AWSCustomRuntime { BlockingQueue> outputQueue = new ArrayBlockingQueue<>(3); + /** Queue for capturing error responses from the Custom Runtime event loop. */ + public BlockingQueue> errorQueue = new ArrayBlockingQueue<>(3); + public AWSCustomRuntime(ConfigurableApplicationContext context) { context.getEnvironment().getPropertySources().addFirst( new MapPropertySource("AWSCustomRuntime", @@ -55,6 +58,11 @@ Consumer> consume() { return v -> outputQueue.offer(v); } + @Bean("2018-06-01/runtime/invocation/consume/error") + Consumer> consumeError() { + return v -> errorQueue.offer(v); + } + @SuppressWarnings("unchecked") @Bean("2018-06-01/runtime/invocation/next") Supplier> supply() { diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/CustomRuntimeEventLoopTest.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/CustomRuntimeEventLoopTest.java index 82da66b30..2ec2530ae 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/CustomRuntimeEventLoopTest.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/CustomRuntimeEventLoopTest.java @@ -16,7 +16,9 @@ package org.springframework.cloud.function.adapter.aws; +import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.function.Function; import org.junit.jupiter.api.Test; @@ -26,6 +28,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.cloud.function.adapter.test.aws.AWSCustomRuntime; +import org.springframework.cloud.function.json.JsonMapper; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -217,6 +220,76 @@ public void test_definitionLookupAndComposition() throws Exception { } + @Test + public void testFunctionThatThrowsException() throws Exception { + try (ConfigurableApplicationContext userContext = + new SpringApplicationBuilder(ErrorFunctionConfiguration.class, AWSCustomRuntime.class) + .web(WebApplicationType.SERVLET) + .properties("_HANDLER=hello", "server.port=0") + .run()) { + + AWSCustomRuntime aws = userContext.getBean(AWSCustomRuntime.class); + JsonMapper mapper = userContext.getBean(JsonMapper.class); + + aws.exchange("\"test\""); + Message errorMessage = aws.errorQueue.poll(5000, java.util.concurrent.TimeUnit.MILLISECONDS); + + assertThat(errorMessage).isNotNull(); + Map errorMap = mapper.fromJson(errorMessage.getPayload(), Map.class); + + assertThat(errorMap) + .containsEntry("errorMessage", "Something went wrong") + .containsEntry("errorType", "java.lang.RuntimeException") + .containsKey("stackTrace"); + assertThat(errorMap.get("stackTrace")).isInstanceOf(List.class); + assertThat((List) errorMap.get("stackTrace")) + .isNotEmpty() + .first().asString().contains("CustomRuntimeEventLoopTest"); + } + } + + @Test + public void testFunctionThatThrowsNestedException() throws Exception { + try (ConfigurableApplicationContext userContext = + new SpringApplicationBuilder(ErrorFunctionConfiguration.class, AWSCustomRuntime.class) + .web(WebApplicationType.SERVLET) + .properties("_HANDLER=helloNested", "server.port=0") + .run()) { + + AWSCustomRuntime aws = userContext.getBean(AWSCustomRuntime.class); + JsonMapper mapper = userContext.getBean(JsonMapper.class); + + aws.exchange("\"test\""); + Message errorMessage = aws.errorQueue.poll(5000, java.util.concurrent.TimeUnit.MILLISECONDS); + + assertThat(errorMessage).isNotNull(); + Map errorMap = mapper.fromJson(errorMessage.getPayload(), Map.class); + + assertThat(errorMap) + .containsEntry("errorMessage", "Wrapper exception") + .containsEntry("errorType", "java.lang.RuntimeException"); + assertThat(errorMap.get("stackTrace")).isInstanceOf(List.class); + } + } + + @EnableAutoConfiguration + protected static class ErrorFunctionConfiguration { + @Bean + public Function hello() { + return event -> { + throw new RuntimeException("Something went wrong"); + }; + } + + @Bean + public Function helloNested() { + return event -> { + Exception cause = new IllegalStateException("Root cause error"); + throw new RuntimeException("Wrapper exception", cause); + }; + } + } + @EnableAutoConfiguration protected static class SingleFunctionConfiguration { @Bean