diff --git a/docs/src/main/asciidoc/spring-cloud-function.adoc b/docs/src/main/asciidoc/spring-cloud-function.adoc index b628076d5..ce748ddb9 100644 --- a/docs/src/main/asciidoc/spring-cloud-function.adoc +++ b/docs/src/main/asciidoc/spring-cloud-function.adoc @@ -691,12 +691,11 @@ plain text and JSON. |=== -As the table above shows the behaviour of the endpoint depends on the method and also the type of incoming request data. When the incoming data -is single valued, and the target function is declared as obviously single valued (i.e. not returning a collection or `Flux`), then the response -will also contain a single value. +As the table above shows the behavior of the endpoint depends on the method and also the type of incoming request data. When the incoming data is single valued, and the target function is declared as obviously single valued (i.e. not returning a collection or `Flux`), then the response will also contain a single value. For multi-valued responses the client can ask for a server-sent event stream by sending `Accept: text/event-stream". -Functions and consumers that are declared with input and output in `Message` will see the request headers on the input messages, and the output message headers will be converted to HTTP headers. +Functions and consumers that are declared with input and output in `Message` will see the request headers as _message headers_, and the output _message headers_ will be converted to HTTP headers. +The _payload_ of the Message will be a `body` or empty string if there is no `body` or it is null. When POSTing text the response format might be different with Spring Boot 2.0 and older versions, depending on the content negotiation (provide content type and accept headers for the best results). @@ -706,8 +705,8 @@ See <> to see the details and example on how to As you have noticed from the previous table, you can pass an argument to a function as path variable (i.e., `/{function}/{item}`). For example, `http://localhost:8080/uppercase/foo` will result in calling `uppercase` function with its input parameter being `foo`. -While this is the recommended approach and the one that fits most use cases cases, there are times when you have to deal with HTTP request parameters. -The framework will treat HTTP request parameters similar to the HTTP headers by storing them in `Message` headers under the header key `http_request_param` +While this is the recommended approach and the one that fits most use cases cases, there are times when you have to deal with HTTP request parameters (e.g., `http://localhost:8080/uppercase/foo?name=Bill`) +The framework will treat HTTP request parameters similar to the HTTP headers by storing them in the `Message` headers under the header key `http_request_param` with its value being a `Map` of request parameters, so in order to access them your function input signature should accept `Message` type (e.g., `Function, String>`). For convenience we provide `HeaderUtils.HTTP_REQUEST_PARAM` constant. === Function Mapping rules diff --git a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/util/FunctionWebRequestProcessingHelper.java b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/util/FunctionWebRequestProcessingHelper.java index 3d589517c..3f876133f 100644 --- a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/util/FunctionWebRequestProcessingHelper.java +++ b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/util/FunctionWebRequestProcessingHelper.java @@ -85,27 +85,27 @@ public final class FunctionWebRequestProcessingHelper { @SuppressWarnings({ "rawtypes", "unchecked" }) public static Publisher processRequest(FunctionWrapper wrapper, Object argument, boolean eventStream) { + if (argument == null) { + argument = ""; + } FunctionInvocationWrapper function = wrapper.getFunction(); HttpHeaders headers = wrapper.getHeaders(); Message inputMessage = null; - if (argument != null) { - MessageBuilder builder = MessageBuilder.withPayload(argument); - if (!CollectionUtils.isEmpty(wrapper.getParams())) { - builder = builder.setHeader(HeaderUtils.HTTP_REQUEST_PARAM, wrapper.getParams().toSingleValueMap()); - } - inputMessage = builder.copyHeaders(headers.toSingleValueMap()).build(); + + MessageBuilder builder = MessageBuilder.withPayload(argument); + if (!CollectionUtils.isEmpty(wrapper.getParams())) { + builder = builder.setHeader(HeaderUtils.HTTP_REQUEST_PARAM, wrapper.getParams().toSingleValueMap()); } + inputMessage = builder.copyHeaders(headers.toSingleValueMap()).build(); if (function.isRoutingFunction()) { function.setSkipOutputConversion(true); } - Object input = argument == null ? "" : (argument instanceof Publisher ? Flux.from((Publisher) argument) : inputMessage); - - Object result = function.apply(input); + Object result = function.apply(inputMessage); if (function.isConsumer()) { if (result instanceof Publisher) { Mono.from((Publisher) result).subscribe(); diff --git a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/function/FunctionEndpointInitializerTests.java b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/function/FunctionEndpointInitializerTests.java index 721ae6e23..79098d809 100644 --- a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/function/FunctionEndpointInitializerTests.java +++ b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/function/FunctionEndpointInitializerTests.java @@ -17,6 +17,11 @@ package org.springframework.cloud.function.web.function; import java.net.URI; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -25,7 +30,9 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.cloud.function.context.FunctionRegistration; import org.springframework.cloud.function.context.FunctionType; @@ -35,6 +42,14 @@ import org.springframework.context.support.GenericApplicationContext; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.util.SocketUtils; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.ResolvableType; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.web.util.UriComponentsBuilder; import static org.assertj.core.api.Assertions.assertThat; @@ -56,6 +71,29 @@ public class FunctionEndpointInitializerTests { public void close() throws Exception { System.clearProperty("server.port"); } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + public void testEmptyBodyRequestParameters() throws Exception { + SpringApplication.run(BeansConfiguration.class); + String port = System.getProperty("server.port"); + TestRestTemplate testRestTemplate = new TestRestTemplate(); + Map params = new HashMap<>(); + params.put("fname", "Jim"); + params.put("lname", "Lahey"); + + HttpHeaders headers = new HttpHeaders(); + headers.set("Accept", "application/json"); + HttpEntity entity = new HttpEntity(headers); + + String urlTemplate = UriComponentsBuilder.fromHttpUrl("http://localhost:" + port + "/nullPayload") + .queryParam("fname", "Jim").queryParam("lname", "Lahey").encode().toUriString(); + + ResponseEntity response = testRestTemplate.exchange(urlTemplate, HttpMethod.GET, entity, String.class); + String res = response.getBody(); + assertThat(res).contains("Jim"); + assertThat(res).contains("Lahey"); + } @Test public void testNonExistingFunction() throws Exception { @@ -144,6 +182,17 @@ public class FunctionEndpointInitializerTests { } + @EnableAutoConfiguration + @Configuration + protected static class BeansConfiguration { + @Bean + public BiFunction, Map> nullPayload() { + return (p, h) -> { + return h; + }; + } + } + @SpringBootConfiguration protected static class ApplicationConfiguration