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