diff --git a/docs/src/main/asciidoc/adapters/gcp-intro.adoc b/docs/src/main/asciidoc/adapters/gcp-intro.adoc index 5c6a06c06..037619ba4 100644 --- a/docs/src/main/asciidoc/adapters/gcp-intro.adoc +++ b/docs/src/main/asciidoc/adapters/gcp-intro.adoc @@ -136,6 +136,28 @@ Invoke the HTTP function: curl https://REGION-PROJECT_ID.cloudfunctions.net/function-sample-gcp-http -d "hello" ---- + +Setting custom HTTP statusCode: + +---- +Users can specify the response statusCode that they want to set by wrapping "statusCode" in headers. +---- + +[source, java] +---- +@Bean +public Function> function() { + + String payload = "hello"; + + Message message = MessageBuilder.withPayload(payload).setHeader("statusCode", 404).build(); + + return input -> message; +}; +---- + + + ==== Background Functions Google Cloud Functions also supports deploying https://cloud.google.com/functions/docs/writing/background[Background Functions] which are invoked indirectly in response to an event, such as a message on a https://cloud.google.com/pubsub[Cloud Pub/Sub] topic, a change in a https://cloud.google.com/storage[Cloud Storage] bucket, or a https://firebase.google.com/[Firebase] event. diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/main/java/org/springframework/cloud/function/adapter/gcp/FunctionInvoker.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/main/java/org/springframework/cloud/function/adapter/gcp/FunctionInvoker.java index 2d1c7cda0..7d50ae084 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/main/java/org/springframework/cloud/function/adapter/gcp/FunctionInvoker.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/main/java/org/springframework/cloud/function/adapter/gcp/FunctionInvoker.java @@ -95,6 +95,8 @@ public class FunctionInvoker extends AbstractSpringFunctionAdapterInitializer, Message> function = lookupFunction(); Message message = getInputType() == Void.class || getInputType() == null ? null @@ -123,8 +125,8 @@ public class FunctionInvoker extends AbstractSpringFunctionAdapterInitializer httpResponse.setContentType(contentType)); - if (headers.containsKey("statusCode")) { - httpResponse.setStatusCode((int) headers.get("statusCode")); + if (headers.containsKey(httpStatusCode) && (headers.get(httpStatusCode) instanceof Integer)) { + httpResponse.setStatusCode((int) headers.get(httpStatusCode)); } } } diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/java/org/springframework/cloud/function/adapter/gcp/FunctionInvokerHttpTests.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/java/org/springframework/cloud/function/adapter/gcp/FunctionInvokerHttpTests.java index 57de8998e..8c17106c6 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/java/org/springframework/cloud/function/adapter/gcp/FunctionInvokerHttpTests.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/java/org/springframework/cloud/function/adapter/gcp/FunctionInvokerHttpTests.java @@ -20,6 +20,8 @@ import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.StringReader; import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -37,7 +39,9 @@ import org.springframework.context.annotation.Import; import org.springframework.messaging.Message; import org.springframework.messaging.support.MessageBuilder; +import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** @@ -52,27 +56,38 @@ public class FunctionInvokerHttpTests { @Test public void testHelloWorldSupplier() throws Exception { - testHttpFunction(HelloWorldSupplier.class, null, "Hello World!"); + testHttpFunction(HelloWorldSupplier.class, null, "Hello World!", false, false); } @Test public void testJsonInputFunction() throws Exception { testHttpFunction(JsonInputFunction.class, new IncomingRequest("hello"), - "Thank you for sending the message: hello"); + "Thank you for sending the message: hello", false, false); } @Test public void testJsonInputOutputFunction() throws Exception { testHttpFunction(JsonInputOutputFunction.class, new IncomingRequest("hello"), - new OutgoingResponse("Thank you for sending the message: hello")); + new OutgoingResponse("Thank you for sending the message: hello"), false, false); } @Test public void testJsonInputConsumer_Background() throws Exception { - testHttpFunction(JsonInputConsumer.class, new IncomingRequest("hello"), null); + testHttpFunction(JsonInputConsumer.class, new IncomingRequest("hello"), null, false, false); } - private void testHttpFunction(Class configurationClass, I input, O expectedOutput) throws Exception { + @Test + public void testStatusCodeSet() throws Exception { + testHttpFunction(statusCodeSupplier.class, "hello", "hello", true, false); + } + + @Test + public void testMultiValueHeaderSupplied() throws Exception { + testHttpFunction(multiValueHeaderSupplier.class, "hello", "hello", false, true); + } + + + private void testHttpFunction(Class configurationClass, I input, O expectedOutput, boolean statusCodeCheck, boolean multiHeaderCheck) throws Exception { try (FunctionInvoker handler = new FunctionInvoker(configurationClass);) { HttpRequest request = Mockito.mock(HttpRequest.class); @@ -88,6 +103,14 @@ public class FunctionInvokerHttpTests { when(response.getWriter()).thenReturn(bufferedWriter); handler.service(request, response); + if (statusCodeCheck) { + verify(response).setStatusCode(404); + } + + if (multiHeaderCheck) { + verify(response).appendHeader("multiValueHeader", "123,headerThing"); + } + // Closing the writer is done by the Framework/caller. bufferedWriter.close(); @@ -108,6 +131,41 @@ public class FunctionInvokerHttpTests { } + @Configuration + @Import({ ContextFunctionCatalogAutoConfiguration.class }) + protected static class statusCodeSupplier { + + @Bean + public Function> function() { + + String payload = "hello"; + + Message msg = MessageBuilder.withPayload(payload).setHeader("statusCode", 404) + .build(); + + return x -> msg; + }; + + } + + @Configuration + @Import({ ContextFunctionCatalogAutoConfiguration.class }) + protected static class multiValueHeaderSupplier { + + @Bean + public Function> function() { + + String payload = "hello"; + List li = new ArrayList(asList(123, "headerThing")); + + Message msg = MessageBuilder.withPayload(payload).setHeader("multiValueHeader", li) + .build(); + + return x -> msg; + }; + + } + @Configuration @Import({ ContextFunctionCatalogAutoConfiguration.class }) protected static class JsonInputFunction { @@ -128,10 +186,9 @@ public class FunctionInvokerHttpTests { return (in) -> { return MessageBuilder .withPayload(new OutgoingResponse("Thank you for sending the message: " + in.message)) - .setHeader("foo", "bar").build(); + .setHeader("statusCode", "bar").build(); }; } - } @Configuration