Merge pull request #875 from ddixit14/user-custom-status-code-fix

Enabling users to set custom Response Code
This commit is contained in:
Oleg Zhurakousky
2022-05-30 13:34:53 +02:00
committed by GitHub
3 changed files with 179 additions and 28 deletions

View File

@@ -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:
----
Functions can specify a custom HTTP response code by setting the `FunctionInvoker.HTTP_STATUS_CODE` header.
----
[source, java]
----
@Bean
public Function<String, Message<String>> function() {
String payload = "hello";
Message<String> message = MessageBuilder.withPayload(payload).setHeader(FunctionInvoker.HTTP_STATUS_CODE, 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.

View File

@@ -18,8 +18,10 @@ package org.springframework.cloud.function.adapter.gcp;
import java.io.BufferedReader;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.Collectors;
import com.google.cloud.functions.Context;
import com.google.cloud.functions.HttpFunction;
@@ -32,6 +34,7 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.function.context.AbstractSpringFunctionAdapterInitializer;
import org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.util.Assert;
import org.springframework.util.MimeTypeUtils;
@@ -51,6 +54,11 @@ public class FunctionInvoker extends AbstractSpringFunctionAdapterInitializer<Ht
private static final Log log = LogFactory.getLog(FunctionInvoker.class);
/**
* Constant specifying Http Status Code. Accessible to users by calling 'FunctionInvoker.HTTP_STATUS_CODE'
*/
public static final String HTTP_STATUS_CODE = "statusCode";
private String functionName = "";
public FunctionInvoker() {
@@ -91,18 +99,37 @@ public class FunctionInvoker extends AbstractSpringFunctionAdapterInitializer<Ht
*/
@Override
public void service(HttpRequest httpRequest, HttpResponse httpResponse) throws Exception {
Function<Message<BufferedReader>, Message<byte[]>> function = lookupFunction();
Message<BufferedReader> message = getInputType() == Void.class || getInputType() == null ? null
: MessageBuilder.withPayload(httpRequest.getReader()).copyHeaders(httpRequest.getHeaders()).build();
: MessageBuilder.withPayload(httpRequest.getReader()).copyHeaders(httpRequest.getHeaders()).build();
Message<byte[]> result = function.apply(message);
if (result != null) {
MessageHeaders headers = result.getHeaders();
httpResponse.getWriter().write(new String(result.getPayload(), StandardCharsets.UTF_8));
for (Entry<String, Object> header : result.getHeaders().entrySet()) {
httpResponse.appendHeader(header.getKey(), header.getValue().toString());
for (Entry<String, Object> header : headers.entrySet()) {
Object values = header.getValue();
if (values instanceof Collection<?>) {
String headerValue = ((Collection<?>) values).stream().map(item -> item.toString()).collect(Collectors.joining(","));
httpResponse.appendHeader(header.getKey(), headerValue);
}
else {
httpResponse.appendHeader(header.getKey(), header.getValue().toString());
}
}
httpRequest.getContentType().ifPresent(contentType -> httpResponse.setContentType(contentType));
if (headers.containsKey(HTTP_STATUS_CODE)) {
if (headers.get(HTTP_STATUS_CODE) instanceof Integer) {
httpResponse.setStatusCode((int) headers.get(HTTP_STATUS_CODE));
}
else {
log.warn("The statusCode should be an Integer value");
}
}
}
}

View File

@@ -18,8 +18,11 @@ package org.springframework.cloud.function.adapter.gcp;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
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;
@@ -27,9 +30,13 @@ import java.util.function.Supplier;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import com.google.gson.Gson;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -37,7 +44,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;
/**
@@ -46,55 +55,114 @@ import static org.mockito.Mockito.when;
* @author Dmitry Solomakha
* @author Mike Eltsufin
*/
@ExtendWith(OutputCaptureExtension.class)
public class FunctionInvokerHttpTests {
private static final Gson gson = new Gson();
private HttpRequest request;
private HttpResponse response;
private BufferedWriter bufferedWriter;
private StringWriter writer;
@BeforeEach
void testSetup() throws IOException {
request = Mockito.mock(HttpRequest.class);
response = Mockito.mock(HttpResponse.class);
writer = new StringWriter();
bufferedWriter = new BufferedWriter(writer);
when(response.getWriter()).thenReturn(bufferedWriter);
}
@Test
public void testHelloWorldSupplier() throws Exception {
testHttpFunction(HelloWorldSupplier.class, null, "Hello World!");
String expectedOutput = "Hello World!";
FunctionInvoker handler = new FunctionInvoker(HelloWorldSupplier.class);
handler.service(request, response);
bufferedWriter.close();
assertThat(writer.toString()).isEqualTo(gson.toJson(expectedOutput));
}
@Test
public void testJsonInputFunction() throws Exception {
testHttpFunction(JsonInputFunction.class, new IncomingRequest("hello"),
"Thank you for sending the message: hello");
FunctionInvoker handler = new FunctionInvoker(JsonInputFunction.class);
String expectedOutput = "Thank you for sending the message: hello";
IncomingRequest input = new IncomingRequest("hello");
when(request.getReader()).thenReturn(new BufferedReader(new StringReader(gson.toJson(input))));
handler.service(request, response);
bufferedWriter.close();
assertThat(writer.toString()).isEqualTo(gson.toJson(expectedOutput));
}
@Test
public void testJsonInputOutputFunction() throws Exception {
testHttpFunction(JsonInputOutputFunction.class, new IncomingRequest("hello"),
new OutgoingResponse("Thank you for sending the message: hello"));
FunctionInvoker handler = new FunctionInvoker(JsonInputOutputFunction.class);
OutgoingResponse expectedOutput = new OutgoingResponse("Thank you for sending the message: hello");
IncomingRequest input = new IncomingRequest("hello");
when(request.getReader()).thenReturn(new BufferedReader(new StringReader(gson.toJson(input))));
handler.service(request, response);
bufferedWriter.close();
assertThat(writer.toString()).isEqualTo(gson.toJson(expectedOutput));
}
@Test
public void testJsonInputConsumer_Background() throws Exception {
testHttpFunction(JsonInputConsumer.class, new IncomingRequest("hello"), null);
public void testJsonInputConsumer_Background(CapturedOutput capturedOutput) throws Exception {
FunctionInvoker handler = new FunctionInvoker(JsonInputConsumer.class);
IncomingRequest input = new IncomingRequest("hello");
when(request.getReader()).thenReturn(new BufferedReader(new StringReader(gson.toJson(input))));
handler.service(request, response);
bufferedWriter.close();
assertThat(capturedOutput.toString()).contains("Thank you for sending the message: hello");
}
private <I, O> void testHttpFunction(Class<?> configurationClass, I input, O expectedOutput) throws Exception {
try (FunctionInvoker handler = new FunctionInvoker(configurationClass);) {
@Test
public void testStatusCodeSet() throws Exception {
HttpRequest request = Mockito.mock(HttpRequest.class);
FunctionInvoker handler = new FunctionInvoker(StatusCodeSupplier.class);
String input = "hello";
when(request.getReader()).thenReturn(new BufferedReader(new StringReader(gson.toJson(input))));
handler.service(request, response);
bufferedWriter.close();
if (input != null) {
when(request.getReader()).thenReturn(new BufferedReader(new StringReader(gson.toJson(input))));
}
verify(response).setStatusCode(404);
HttpResponse response = Mockito.mock(HttpResponse.class);
StringWriter writer = new StringWriter();
}
BufferedWriter bufferedWriter = new BufferedWriter(writer);
when(response.getWriter()).thenReturn(bufferedWriter);
handler.service(request, response);
@Test
public void testMultiValueHeaderSupplied() throws Exception {
// Closing the writer is done by the Framework/caller.
bufferedWriter.close();
FunctionInvoker handler = new FunctionInvoker(MultiValueHeaderSupplier.class);
String input = "hello";
when(request.getReader()).thenReturn(new BufferedReader(new StringReader(gson.toJson(input))));
handler.service(request, response);
bufferedWriter.close();
if (expectedOutput != null) {
assertThat(writer.toString()).isEqualTo(gson.toJson(expectedOutput));
}
}
verify(response).appendHeader("multiValueHeader", "123,headerThing");
}
@Configuration
@@ -108,6 +176,41 @@ public class FunctionInvokerHttpTests {
}
@Configuration
@Import({ ContextFunctionCatalogAutoConfiguration.class })
protected static class StatusCodeSupplier {
@Bean
public Function<String, Message<String>> function() {
String payload = "hello";
Message<String> msg = MessageBuilder.withPayload(payload).setHeader("statusCode", 404)
.build();
return x -> msg;
};
}
@Configuration
@Import({ ContextFunctionCatalogAutoConfiguration.class })
protected static class MultiValueHeaderSupplier {
@Bean
public Function<String, Message<String>> function() {
String payload = "hello";
List<Object> li = new ArrayList<Object>(asList(123, "headerThing"));
Message<String> msg = MessageBuilder.withPayload(payload).setHeader("multiValueHeader", li)
.build();
return x -> msg;
};
}
@Configuration
@Import({ ContextFunctionCatalogAutoConfiguration.class })
protected static class JsonInputFunction {
@@ -131,7 +234,6 @@ public class FunctionInvokerHttpTests {
.setHeader("foo", "bar").build();
};
}
}
@Configuration