Skip to content

Commit ffd4e43

Browse files
committed
spring-cloudGH-716 Add support for returning custom status code
Resolves spring-cloud#716
1 parent ce1265d commit ffd4e43

File tree

7 files changed

+146
-31
lines changed

7 files changed

+146
-31
lines changed

spring-cloud-function-adapters/spring-cloud-function-adapter-aws/pom.xml

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,6 @@
2727
</properties>
2828

2929
<dependencies>
30-
<dependency>
31-
<groupId>com.amazonaws</groupId>
32-
<artifactId>amazon-kinesis-client</artifactId>
33-
<version>1.14.4</version>
34-
</dependency>
3530
<dependency>
3631
<groupId>org.springframework.cloud</groupId>
3732
<artifactId>spring-cloud-function-context</artifactId>
@@ -73,6 +68,12 @@
7368
<version>1.2.1</version>
7469
<scope>provided</scope>
7570
</dependency>
71+
<dependency>
72+
<groupId>com.amazonaws</groupId>
73+
<artifactId>amazon-kinesis-client</artifactId>
74+
<version>1.14.4</version>
75+
<scope>provided</scope>
76+
</dependency>
7677
<dependency>
7778
<groupId>com.amazonaws</groupId>
7879
<artifactId>aws-lambda-java-events</artifactId>
@@ -97,6 +98,7 @@
9798
</exclusion>
9899
</exclusions>
99100
<optional>true</optional>
101+
<scope>provided</scope>
100102
</dependency>
101103
<dependency>
102104
<groupId>org.springframework.boot</groupId>

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@
2929

3030
import com.amazonaws.services.lambda.runtime.Context;
3131
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
32+
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
3233
import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent;
34+
import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPResponse;
3335
import com.amazonaws.services.lambda.runtime.events.KinesisEvent;
3436
import com.amazonaws.services.lambda.runtime.events.S3Event;
3537
import com.amazonaws.services.lambda.runtime.events.SNSEvent;
@@ -163,7 +165,13 @@ else if (request instanceof Iterable) {
163165

164166
@SuppressWarnings({ "rawtypes", "unchecked" })
165167
public static byte[] generateOutput(Message requestMessage, Message<byte[]> responseMessage,
166-
ObjectMapper objectMapper) {
168+
ObjectMapper objectMapper, Type functionOutputType) {
169+
170+
Class<?> outputClass = FunctionTypeUtils.getRawType(functionOutputType);
171+
if (outputClass != null && (APIGatewayV2HTTPResponse.class.isAssignableFrom(outputClass)
172+
|| APIGatewayProxyResponseEvent.class.isAssignableFrom(outputClass))) {
173+
return responseMessage.getPayload();
174+
}
167175

168176

169177
if (!objectMapper.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES)) {
@@ -190,7 +198,7 @@ public static byte[] generateOutput(Message requestMessage, Message<byte[]> resp
190198
}
191199

192200
String body = responseMessage == null
193-
? "\"OK\"" : new String(responseMessage.getPayload(), StandardCharsets.UTF_8).replaceAll("\\\"", "\"");
201+
? "\"OK\"" : new String(responseMessage.getPayload(), StandardCharsets.UTF_8).replaceAll("\\\"", "");
194202
response.put("body", body);
195203

196204
if (responseMessage != null) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ static void eventLoop(ApplicationContext context) {
102102
logger.debug("Reply from function: " + responseMessage);
103103
}
104104

105-
byte[] outputBody = AWSLambdaUtils.generateOutput(eventMessage, responseMessage, mapper);
105+
byte[] outputBody = AWSLambdaUtils.generateOutput(eventMessage, responseMessage, mapper, function.getOutputType());
106106
ResponseEntity<Object> result = rest
107107
.exchange(RequestEntity.post(URI.create(invocationUrl)).body(outputBody), Object.class);
108108

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

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
import com.amazonaws.services.lambda.runtime.Context;
2828
import com.amazonaws.services.lambda.runtime.RequestStreamHandler;
29+
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
2930
import com.fasterxml.jackson.databind.ObjectMapper;
3031
import org.apache.commons.logging.Log;
3132
import org.apache.commons.logging.LogFactory;
@@ -39,6 +40,7 @@
3940
import org.springframework.cloud.function.utils.FunctionClassUtils;
4041
import org.springframework.context.ConfigurableApplicationContext;
4142
import org.springframework.core.env.Environment;
43+
import org.springframework.http.HttpStatus;
4244
import org.springframework.messaging.Message;
4345
import org.springframework.messaging.MessageHeaders;
4446
import org.springframework.messaging.support.MessageBuilder;
@@ -79,10 +81,22 @@ public void handleRequest(InputStream input, OutputStream output, Context contex
7981
Message requestMessage = AWSLambdaUtils
8082
.generateMessage(payload, new MessageHeaders(Collections.emptyMap()), function.getInputType(), this.objectMapper, context);
8183

82-
Object response = this.function.apply(requestMessage);
84+
try {
85+
Object response = this.function.apply(requestMessage);
86+
byte[] responseBytes = this.buildResult(requestMessage, response);
87+
StreamUtils.copy(responseBytes, output);
88+
}
89+
catch (Exception e) {
90+
logger.error(e);
91+
StreamUtils.copy(this.buildExceptionResult(requestMessage, e), output);
92+
}
93+
}
8394

84-
byte[] responseBytes = this.buildResult(requestMessage, response);
85-
StreamUtils.copy(responseBytes, output);
95+
private byte[] buildExceptionResult(Message<?> requestMessage, Exception exception) throws IOException {
96+
APIGatewayProxyResponseEvent event = new APIGatewayProxyResponseEvent();
97+
event.setStatusCode(HttpStatus.EXPECTATION_FAILED.value());
98+
event.setBody(exception.getMessage());
99+
return this.objectMapper.writeValueAsBytes(event);
86100
}
87101

88102
@SuppressWarnings("unchecked")
@@ -113,7 +127,7 @@ private byte[] buildResult(Message<?> requestMessage, Object output) throws IOEx
113127
else {
114128
responseMessage = (Message<byte[]>) output;
115129
}
116-
return AWSLambdaUtils.generateOutput(requestMessage, responseMessage, this.objectMapper);
130+
return AWSLambdaUtils.generateOutput(requestMessage, responseMessage, this.objectMapper, function.getOutputType());
117131
}
118132

119133
private void start() {

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

Lines changed: 98 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@
2121
import java.io.InputStream;
2222
import java.lang.reflect.Field;
2323
import java.nio.charset.StandardCharsets;
24+
import java.util.Collections;
2425
import java.util.Map;
2526
import java.util.function.Consumer;
2627
import java.util.function.Function;
2728
import java.util.function.Supplier;
2829

2930
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
31+
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
3032
import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent;
33+
import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPResponse;
3134
import com.amazonaws.services.lambda.runtime.events.KinesisEvent;
3235
import com.amazonaws.services.lambda.runtime.events.S3Event;
3336
import com.amazonaws.services.lambda.runtime.events.SNSEvent;
@@ -46,7 +49,6 @@
4649
import org.springframework.util.MimeType;
4750

4851
import static org.assertj.core.api.Assertions.assertThat;
49-
import static org.junit.jupiter.api.Assertions.fail;
5052

5153
/**
5254
*
@@ -698,7 +700,7 @@ public void testApiGatewayStringEventBody() throws Exception {
698700
invoker.handleRequest(targetStream, output, null);
699701
ObjectMapper mapper = new ObjectMapper();
700702
Map result = mapper.readValue(output.toByteArray(), Map.class);
701-
assertThat(result.get("body")).isEqualTo("\"HELLO\"");
703+
assertThat(result.get("body")).isEqualTo("HELLO");
702704
}
703705

704706
@SuppressWarnings("rawtypes")
@@ -713,7 +715,7 @@ public void testApiGatewayMapEventBody() throws Exception {
713715
invoker.handleRequest(targetStream, output, null);
714716

715717
Map result = mapper.readValue(output.toByteArray(), Map.class);
716-
assertThat(result.get("body")).isEqualTo("\"JIM LAHEY\"");
718+
assertThat(result.get("body")).isEqualTo("JIM LAHEY");
717719
}
718720

719721
@SuppressWarnings("rawtypes")
@@ -729,7 +731,7 @@ public void testApiGatewayEvent() throws Exception {
729731

730732
Map result = mapper.readValue(output.toByteArray(), Map.class);
731733
System.out.println(result);
732-
assertThat(result.get("body")).isEqualTo("\"hello\"");
734+
assertThat(result.get("body")).isEqualTo("hello");
733735
}
734736

735737
@SuppressWarnings("rawtypes")
@@ -745,7 +747,7 @@ public void testApiGatewayV2Event() throws Exception {
745747

746748
Map result = mapper.readValue(output.toByteArray(), Map.class);
747749
System.out.println(result);
748-
assertThat(result.get("body")).isEqualTo("\"Hello from Lambda\"");
750+
assertThat(result.get("body")).isEqualTo("Hello from Lambda");
749751
}
750752

751753
@SuppressWarnings("rawtypes")
@@ -761,9 +763,63 @@ public void testApiGatewayAsSupplier() throws Exception {
761763

762764
Map result = mapper.readValue(output.toByteArray(), Map.class);
763765
System.out.println(result);
764-
assertThat(result.get("body")).isEqualTo("\"boom\"");
766+
assertThat(result.get("body")).isEqualTo("boom");
765767
}
766768

769+
@SuppressWarnings("rawtypes")
770+
@Test
771+
public void testApiGatewayInAndOut() throws Exception {
772+
System.setProperty("MAIN_CLASS", ApiGatewayConfiguration.class.getName());
773+
System.setProperty("spring.cloud.function.definition", "inputOutputApiEvent");
774+
FunctionInvoker invoker = new FunctionInvoker();
775+
776+
InputStream targetStream = new ByteArrayInputStream(this.apiGatewayEvent.getBytes());
777+
ByteArrayOutputStream output = new ByteArrayOutputStream();
778+
invoker.handleRequest(targetStream, output, null);
779+
780+
Map result = mapper.readValue(output.toByteArray(), Map.class);
781+
assertThat(result.get("body")).isEqualTo("hello");
782+
Map headers = (Map) result.get("headers");
783+
assertThat(headers.get("foo")).isEqualTo("bar");
784+
}
785+
786+
@SuppressWarnings("rawtypes")
787+
@Test
788+
public void testApiGatewayInAndOutV2() throws Exception {
789+
System.setProperty("MAIN_CLASS", ApiGatewayConfiguration.class.getName());
790+
System.setProperty("spring.cloud.function.definition", "inputOutputApiEventV2");
791+
FunctionInvoker invoker = new FunctionInvoker();
792+
793+
InputStream targetStream = new ByteArrayInputStream(this.apiGatewayEvent.getBytes());
794+
ByteArrayOutputStream output = new ByteArrayOutputStream();
795+
invoker.handleRequest(targetStream, output, null);
796+
797+
Map result = mapper.readValue(output.toByteArray(), Map.class);
798+
assertThat(result.get("body")).isEqualTo("hello");
799+
Map headers = (Map) result.get("headers");
800+
assertThat(headers.get("foo")).isEqualTo("bar");
801+
}
802+
803+
// @SuppressWarnings("rawtypes")
804+
// @Test
805+
// public void testApiGatewayInAndOutWithException() throws Exception {
806+
// System.setProperty("MAIN_CLASS", ApiGatewayConfiguration.class.getName());
807+
// System.setProperty("spring.cloud.function.definition", "inputOutputApiEventException");
808+
// FunctionInvoker invoker = new FunctionInvoker();
809+
//
810+
// InputStream targetStream = new ByteArrayInputStream(this.apiGatewayEvent.getBytes());
811+
// ByteArrayOutputStream output = new ByteArrayOutputStream();
812+
// invoker.handleRequest(targetStream, output, null);
813+
//
814+
// Map result = mapper.readValue(output.toByteArray(), Map.class);
815+
// assertThat(result.get("body")).isEqualTo("Intentional");
816+
//
817+
// Map headers = (Map) result.get("headers");
818+
// assertThat(headers.get("foo")).isEqualTo("bar");
819+
// }
820+
821+
822+
767823
@SuppressWarnings("rawtypes")
768824
@Test
769825
public void testApiGatewayEventAsMessage() throws Exception {
@@ -777,7 +833,7 @@ public void testApiGatewayEventAsMessage() throws Exception {
777833

778834
Map result = mapper.readValue(output.toByteArray(), Map.class);
779835
System.out.println(result);
780-
assertThat(result.get("body")).isEqualTo("\"hello\"");
836+
assertThat(result.get("body")).isEqualTo("hello");
781837
}
782838

783839
@SuppressWarnings("rawtypes")
@@ -793,7 +849,7 @@ public void testApiGatewayEventAsMap() throws Exception {
793849

794850
Map result = mapper.readValue(output.toByteArray(), Map.class);
795851
System.out.println(result);
796-
assertThat(result.get("body")).isEqualTo("\"hello\"");
852+
assertThat(result.get("body")).isEqualTo("hello");
797853
}
798854

799855
@SuppressWarnings("rawtypes")
@@ -818,13 +874,9 @@ public void testWithDefaultRoutingFailure() throws Exception {
818874

819875
InputStream targetStream = new ByteArrayInputStream(this.apiGatewayEvent.getBytes());
820876
ByteArrayOutputStream output = new ByteArrayOutputStream();
821-
try {
822-
invoker.handleRequest(targetStream, output, null);
823-
fail();
824-
}
825-
catch (Exception e) {
826-
// success, since no definition nor routing instructions are provided
827-
}
877+
invoker.handleRequest(targetStream, output, null);
878+
Map result = mapper.readValue(output.toByteArray(), Map.class);
879+
assertThat(((String) result.get("body"))).startsWith("Failed to establish route, since neither were provided:");
828880
}
829881

830882
@SuppressWarnings("rawtypes")
@@ -839,7 +891,7 @@ public void testWithDefaultRouting() throws Exception {
839891
invoker.handleRequest(targetStream, output, null);
840892

841893
Map result = mapper.readValue(output.toByteArray(), Map.class);
842-
assertThat(result.get("body")).isEqualTo("\"olleh\"");
894+
assertThat(result.get("body")).isEqualTo("olleh");
843895
}
844896

845897
@SuppressWarnings("rawtypes")
@@ -855,7 +907,7 @@ public void testWithDefinitionEnvVariable() throws Exception {
855907
invoker.handleRequest(targetStream, output, null);
856908

857909
Map result = mapper.readValue(output.toByteArray(), Map.class);
858-
assertThat(result.get("body")).isEqualTo("\"OLLEH\"");
910+
assertThat(result.get("body")).isEqualTo("OLLEH");
859911
}
860912

861913
@SuppressWarnings("unchecked")
@@ -1086,6 +1138,35 @@ public Function<APIGatewayProxyRequestEvent, String> inputApiEvent() {
10861138
};
10871139
}
10881140

1141+
@Bean
1142+
public Function<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> inputOutputApiEvent() {
1143+
return v -> {
1144+
APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent();
1145+
response.setBody(v.getBody());
1146+
response.setStatusCode(200);
1147+
response.setHeaders(Collections.singletonMap("foo", "bar"));
1148+
return response;
1149+
};
1150+
}
1151+
1152+
@Bean
1153+
public Function<APIGatewayV2HTTPEvent, APIGatewayV2HTTPResponse> inputOutputApiEventV2() {
1154+
return v -> {
1155+
APIGatewayV2HTTPResponse response = new APIGatewayV2HTTPResponse();
1156+
response.setBody(v.getBody());
1157+
response.setStatusCode(200);
1158+
response.setHeaders(Collections.singletonMap("foo", "bar"));
1159+
return response;
1160+
};
1161+
}
1162+
1163+
@Bean
1164+
public Function<APIGatewayV2HTTPEvent, APIGatewayV2HTTPResponse> inputOutputApiEventException() {
1165+
return v -> {
1166+
throw new IllegalStateException("Intentional");
1167+
};
1168+
}
1169+
10891170
@Bean
10901171
public Function<APIGatewayV2HTTPEvent, String> inputApiV2Event() {
10911172
return v -> {

spring-cloud-function-samples/function-sample-aws/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
2525
<java.version>1.8</java.version>
2626
<wrapper.version>1.0.17.RELEASE</wrapper.version>
27-
<aws-lambda-events.version>2.0.2</aws-lambda-events.version>
27+
<aws-lambda-events.version>3.9.0</aws-lambda-events.version>
2828
<spring-cloud-function.version>3.2.0-SNAPSHOT</spring-cloud-function.version>
2929
</properties>
3030

spring-cloud-function-samples/function-sample-aws/src/main/java/example/FunctionConfiguration.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
import org.springframework.boot.autoconfigure.SpringBootApplication;
77
import org.springframework.context.annotation.Bean;
88

9+
import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent;
10+
import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPResponse;
11+
912
@SpringBootApplication
1013
public class FunctionConfiguration {
1114

@@ -19,6 +22,13 @@ public static void main(String[] args) {
1922

2023
@Bean
2124
public Function<String, String> uppercase() {
22-
return value -> value.toUpperCase();
25+
return value -> {
26+
if (value.equals("exception")) {
27+
throw new RuntimeException("Intentional exception which should result in HTTP 417");
28+
}
29+
else {
30+
return value.toUpperCase();
31+
}
32+
};
2333
}
2434
}

0 commit comments

Comments
 (0)