Skip to content

Commit e680ffd

Browse files
akenraolegz
authored andcommitted
GH-1428 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 Resolves #1433 Signed-off-by: akenra <37288280+akenra@users.noreply.github.com>
1 parent 0c1e9ae commit e680ffd

3 files changed

Lines changed: 92 additions & 8 deletions

File tree

‎spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/CustomRuntimeEventLoop.java‎

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,17 @@
1717
package org.springframework.cloud.function.adapter.aws;
1818

1919
import java.io.ByteArrayInputStream;
20-
import java.io.PrintWriter;
21-
import java.io.StringWriter;
2220
import java.net.SocketException;
2321
import java.net.URI;
2422
import java.nio.charset.StandardCharsets;
2523
import java.text.MessageFormat;
24+
import java.util.Arrays;
2625
import java.util.HashMap;
26+
import java.util.List;
2727
import java.util.Map;
2828
import java.util.concurrent.ExecutorService;
2929
import java.util.concurrent.Executors;
30+
import java.util.stream.Collectors;
3031

3132
import com.amazonaws.services.lambda.runtime.ClientContext;
3233
import com.amazonaws.services.lambda.runtime.CognitoIdentity;
@@ -72,6 +73,8 @@ public final class CustomRuntimeEventLoop implements SmartLifecycle {
7273
private static final String LAMBDA_ERROR_URL_TEMPLATE = "http://{0}/{1}/runtime/invocation/{2}/error";
7374
private static final String LAMBDA_RUNTIME_URL_TEMPLATE = "http://{0}/{1}/runtime/invocation/next";
7475
private static final String LAMBDA_INVOCATION_URL_TEMPLATE = "http://{0}/{1}/runtime/invocation/{2}/response";
76+
private static final String LAMBDA_RUNTIME_FUNCTION_ERROR_TYPE = "Lambda-Runtime-Function-Error-Type";
77+
7578
private static final String USER_AGENT_VALUE = String.format(
7679
"spring-cloud-function/%s-%s",
7780
System.getProperty("java.runtime.version"),
@@ -282,12 +285,11 @@ public String toString() {
282285

283286
private void propagateAwsError(String requestId, Exception e, JsonMapper mapper, String runtimeApi, RestTemplate rest) {
284287
String errorMessage = e.getMessage();
285-
String errorType = e.getClass().getSimpleName();
286-
StringWriter sw = new StringWriter();
287-
PrintWriter pw = new PrintWriter(sw);
288-
e.printStackTrace(pw);
289-
String stackTrace = sw.toString();
290-
Map<String, String> em = new HashMap<>();
288+
String errorType = e.getClass().getName();
289+
List<String> stackTrace = Arrays.stream(e.getStackTrace())
290+
.map(StackTraceElement::toString)
291+
.collect(Collectors.toList());
292+
Map<String, Object> em = new HashMap<>();
291293
em.put("errorMessage", errorMessage);
292294
em.put("errorType", errorType);
293295
em.put("stackTrace", stackTrace);
@@ -296,6 +298,7 @@ private void propagateAwsError(String requestId, Exception e, JsonMapper mapper,
296298
String errorUrl = MessageFormat.format(LAMBDA_ERROR_URL_TEMPLATE, runtimeApi, LAMBDA_VERSION_DATE, requestId);
297299
ResponseEntity<Object> result = rest.exchange(RequestEntity.post(URI.create(errorUrl))
298300
.header(USER_AGENT, USER_AGENT_VALUE)
301+
.header(LAMBDA_RUNTIME_FUNCTION_ERROR_TYPE, errorType)
299302
.body(outputBody), Object.class);
300303
if (logger.isInfoEnabled()) {
301304
logger.info("Result ERROR status: " + result.getStatusCode());

‎spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/test/aws/AWSCustomRuntime.java‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ public class AWSCustomRuntime {
4444

4545
BlockingQueue<Message<String>> outputQueue = new ArrayBlockingQueue<>(3);
4646

47+
/** Queue for capturing error responses from the Custom Runtime event loop. */
48+
public BlockingQueue<Message<String>> errorQueue = new ArrayBlockingQueue<>(3);
49+
4750
public AWSCustomRuntime(ConfigurableApplicationContext context) {
4851
context.getEnvironment().getPropertySources().addFirst(
4952
new MapPropertySource("AWSCustomRuntime",
@@ -55,6 +58,11 @@ Consumer<Message<String>> consume() {
5558
return v -> outputQueue.offer(v);
5659
}
5760

61+
@Bean("2018-06-01/runtime/invocation/consume/error")
62+
Consumer<Message<String>> consumeError() {
63+
return v -> errorQueue.offer(v);
64+
}
65+
5866
@SuppressWarnings("unchecked")
5967
@Bean("2018-06-01/runtime/invocation/next")
6068
Supplier<Message<String>> supply() {

‎spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/CustomRuntimeEventLoopTest.java‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616

1717
package org.springframework.cloud.function.adapter.aws;
1818

19+
import java.util.List;
1920
import java.util.Locale;
21+
import java.util.Map;
2022
import java.util.function.Function;
2123

2224
import org.junit.jupiter.api.Test;
@@ -26,6 +28,7 @@
2628
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
2729
import org.springframework.boot.builder.SpringApplicationBuilder;
2830
import org.springframework.cloud.function.adapter.test.aws.AWSCustomRuntime;
31+
import org.springframework.cloud.function.json.JsonMapper;
2932
import org.springframework.context.ConfigurableApplicationContext;
3033
import org.springframework.context.annotation.Bean;
3134
import org.springframework.context.annotation.Configuration;
@@ -217,6 +220,76 @@ public void test_definitionLookupAndComposition() throws Exception {
217220

218221
}
219222

223+
@Test
224+
public void testFunctionThatThrowsException() throws Exception {
225+
try (ConfigurableApplicationContext userContext =
226+
new SpringApplicationBuilder(ErrorFunctionConfiguration.class, AWSCustomRuntime.class)
227+
.web(WebApplicationType.SERVLET)
228+
.properties("_HANDLER=hello", "server.port=0")
229+
.run()) {
230+
231+
AWSCustomRuntime aws = userContext.getBean(AWSCustomRuntime.class);
232+
JsonMapper mapper = userContext.getBean(JsonMapper.class);
233+
234+
aws.exchange("\"test\"");
235+
Message<String> errorMessage = aws.errorQueue.poll(5000, java.util.concurrent.TimeUnit.MILLISECONDS);
236+
237+
assertThat(errorMessage).isNotNull();
238+
Map<String, Object> errorMap = mapper.fromJson(errorMessage.getPayload(), Map.class);
239+
240+
assertThat(errorMap)
241+
.containsEntry("errorMessage", "Something went wrong")
242+
.containsEntry("errorType", "java.lang.RuntimeException")
243+
.containsKey("stackTrace");
244+
assertThat(errorMap.get("stackTrace")).isInstanceOf(List.class);
245+
assertThat((List<?>) errorMap.get("stackTrace"))
246+
.isNotEmpty()
247+
.first().asString().contains("CustomRuntimeEventLoopTest");
248+
}
249+
}
250+
251+
@Test
252+
public void testFunctionThatThrowsNestedException() throws Exception {
253+
try (ConfigurableApplicationContext userContext =
254+
new SpringApplicationBuilder(ErrorFunctionConfiguration.class, AWSCustomRuntime.class)
255+
.web(WebApplicationType.SERVLET)
256+
.properties("_HANDLER=helloNested", "server.port=0")
257+
.run()) {
258+
259+
AWSCustomRuntime aws = userContext.getBean(AWSCustomRuntime.class);
260+
JsonMapper mapper = userContext.getBean(JsonMapper.class);
261+
262+
aws.exchange("\"test\"");
263+
Message<String> errorMessage = aws.errorQueue.poll(5000, java.util.concurrent.TimeUnit.MILLISECONDS);
264+
265+
assertThat(errorMessage).isNotNull();
266+
Map<String, Object> errorMap = mapper.fromJson(errorMessage.getPayload(), Map.class);
267+
268+
assertThat(errorMap)
269+
.containsEntry("errorMessage", "Wrapper exception")
270+
.containsEntry("errorType", "java.lang.RuntimeException");
271+
assertThat(errorMap.get("stackTrace")).isInstanceOf(List.class);
272+
}
273+
}
274+
275+
@EnableAutoConfiguration
276+
protected static class ErrorFunctionConfiguration {
277+
@Bean
278+
public Function<String, String> hello() {
279+
return event -> {
280+
throw new RuntimeException("Something went wrong");
281+
};
282+
}
283+
284+
@Bean
285+
public Function<String, String> helloNested() {
286+
return event -> {
287+
Exception cause = new IllegalStateException("Root cause error");
288+
throw new RuntimeException("Wrapper exception", cause);
289+
};
290+
}
291+
}
292+
220293
@EnableAutoConfiguration
221294
protected static class SingleFunctionConfiguration {
222295
@Bean

0 commit comments

Comments
 (0)