From c6fe79fc350229401eeb9b892c1609c3420f18b0 Mon Sep 17 00:00:00 2001 From: Oleg Zhurakousky Date: Tue, 30 Jan 2024 15:27:39 +0100 Subject: [PATCH] GH-754 Fix request parameter parsing While concentrating on REST we somehow ignored standard request parameters. This fix addresses it for both v1 and v2 requests as well as adds tests and additional endpoint to a sample to verify Resolves #754 --- .../spring/AwsSpringHttpProcessingUtils.java | 22 ++++++++++++++++- ...DelegatingLambdaContainerHandlerTests.java | 24 ++++++++++++++++++- .../spring/servletapp/ServletApplication.java | 18 ++++++++++++-- samples/springboot3/alt-pet-store/README.md | 19 ++++++++++++++- samples/springboot3/alt-pet-store/pom.xml | 6 +++++ .../controller/PetsController.java | 13 ++++++++++ .../springboot3/alt-pet-store/template.yml | 2 +- 7 files changed, 98 insertions(+), 6 deletions(-) diff --git a/aws-serverless-java-container-springboot3/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringHttpProcessingUtils.java b/aws-serverless-java-container-springboot3/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringHttpProcessingUtils.java index d268bd2e4..c7e507f39 100644 --- a/aws-serverless-java-container-springboot3/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringHttpProcessingUtils.java +++ b/aws-serverless-java-container-springboot3/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringHttpProcessingUtils.java @@ -2,7 +2,10 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.util.Iterator; import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -10,6 +13,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.cloud.function.serverless.web.ServerlessHttpServletRequest; import org.springframework.cloud.function.serverless.web.ServerlessMVC; +import org.springframework.util.CollectionUtils; import org.springframework.util.FileCopyUtils; import org.springframework.util.MultiValueMapAdapter; import org.springframework.util.StringUtils; @@ -109,6 +113,9 @@ private static HttpServletRequest generateRequest1(String request, Context lambd AwsProxyRequest v1Request = readValue(request, AwsProxyRequest.class, mapper); ServerlessHttpServletRequest httpRequest = new ServerlessHttpServletRequest(servletContext, v1Request.getHttpMethod(), v1Request.getPath()); + + populateQueryStringparameters(v1Request.getQueryStringParameters(), httpRequest); + if (v1Request.getMultiValueHeaders() != null) { MultiValueMapAdapter headers = new MultiValueMapAdapter(v1Request.getMultiValueHeaders()); httpRequest.setHeaders(headers); @@ -128,16 +135,21 @@ private static HttpServletRequest generateRequest1(String request, Context lambd securityWriter.writeSecurityContext(v1Request, lambdaContext)); return httpRequest; } + + @SuppressWarnings({ "rawtypes", "unchecked" }) private static HttpServletRequest generateRequest2(String request, Context lambdaContext, SecurityContextWriter securityWriter, ObjectMapper mapper, ServletContext servletContext) { HttpApiV2ProxyRequest v2Request = readValue(request, HttpApiV2ProxyRequest.class, mapper); + + ServerlessHttpServletRequest httpRequest = new ServerlessHttpServletRequest(servletContext, v2Request.getRequestContext().getHttp().getMethod(), v2Request.getRequestContext().getHttp().getPath()); + populateQueryStringparameters(v2Request.getQueryStringParameters(), httpRequest); v2Request.getHeaders().forEach(httpRequest::setHeader); - + if (StringUtils.hasText(v2Request.getBody())) { httpRequest.setContentType("application/json"); httpRequest.setContent(v2Request.getBody().getBytes(StandardCharsets.UTF_8)); @@ -151,6 +163,14 @@ private static HttpServletRequest generateRequest2(String request, Context lambd return httpRequest; } + private static void populateQueryStringparameters(Map requestParameters, ServerlessHttpServletRequest httpRequest) { + if (!CollectionUtils.isEmpty(requestParameters)) { + for (Entry entry : requestParameters.entrySet()) { + httpRequest.setParameter(entry.getKey(), entry.getValue()); + } + } + } + private static T readValue(String json, Class clazz, ObjectMapper mapper) { try { return mapper.readValue(json, clazz); diff --git a/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/SpringDelegatingLambdaContainerHandlerTests.java b/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/SpringDelegatingLambdaContainerHandlerTests.java index dabc30e24..ff1f209b5 100644 --- a/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/SpringDelegatingLambdaContainerHandlerTests.java +++ b/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/SpringDelegatingLambdaContainerHandlerTests.java @@ -13,6 +13,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.cloud.function.serverless.web.ServerlessServletContext; import org.springframework.util.CollectionUtils; import com.amazonaws.serverless.proxy.spring.servletapp.MessageData; @@ -72,12 +73,16 @@ public class SpringDelegatingLambdaContainerHandlerTests { + " },\n" + " \"queryStringParameters\": {\n" + " \"abc\": \"xyz\",\n" + + " \"name\": \"Ricky\",\n" + " \"foo\": \"baz\"\n" + " },\n" + " \"multiValueQueryStringParameters\": {\n" + " \"abc\": [\n" + " \"xyz\"\n" + " ],\n" + + " \"name\": [\n" + + " \"Ricky\"\n" + + " ],\n" + " \"foo\": [\n" + " \"bar\",\n" + " \"baz\"\n" @@ -124,7 +129,7 @@ public class SpringDelegatingLambdaContainerHandlerTests { " \"version\": \"2.0\",\n" + " \"routeKey\": \"$default\",\n" + " \"rawPath\": \"/my/path\",\n" + - " \"rawQueryString\": \"parameter1=value1¶meter1=value2¶meter2=value\",\n" + + " \"rawQueryString\": \"parameter1=value1¶meter1=value2&name=Ricky¶meter2=value\",\n" + " \"cookies\": [\n" + " \"cookie1\",\n" + " \"cookie2\"\n" + @@ -135,6 +140,7 @@ public class SpringDelegatingLambdaContainerHandlerTests { " },\n" + " \"queryStringParameters\": {\n" + " \"parameter1\": \"value1,value2\",\n" + + " \"name\": \"Ricky\",\n" + " \"parameter2\": \"value\"\n" + " },\n" + " \"requestContext\": {\n" + @@ -202,6 +208,22 @@ public static Collection data() { return Arrays.asList(new String[]{API_GATEWAY_EVENT, API_GATEWAY_EVENT_V2}); } + @MethodSource("data") + @ParameterizedTest + public void validateComplesrequest(String jsonEvent) throws Exception { + initServletAppTest(); + InputStream targetStream = new ByteArrayInputStream(this.generateHttpRequest(jsonEvent, "POST", + "/foo/male/list/24", "{\"name\":\"bob\"}", null)); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + handler.handleRequest(targetStream, output, null); + Map result = mapper.readValue(output.toString(StandardCharsets.UTF_8), Map.class); + assertEquals(200, result.get("statusCode")); + String[] respponseBody = ((String) result.get("body")).split("/"); + assertEquals("male", respponseBody[0]); + assertEquals("24", respponseBody[1]); + assertEquals("Ricky", respponseBody[2]); + } + @MethodSource("data") @ParameterizedTest public void testAsyncPost(String jsonEvent) throws Exception { diff --git a/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/ServletApplication.java b/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/ServletApplication.java index 0cb001ed1..9f01859aa 100644 --- a/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/ServletApplication.java +++ b/aws-serverless-java-container-springboot3/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/ServletApplication.java @@ -1,9 +1,13 @@ package com.amazonaws.serverless.proxy.spring.servletapp; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; -import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; @SpringBootApplication(exclude = { org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration.class, @@ -14,5 +18,15 @@ org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration.class }) @Import(MessageController.class) +@RestController public class ServletApplication { + + @RequestMapping(path = "/foo/{gender}/list/{age}", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) + public String complexRequest( + @PathVariable("gender") String gender, + @PathVariable("age") String age, + @RequestParam("name") String name + ) { + return gender + "/" + age + "/" + name; + } } diff --git a/samples/springboot3/alt-pet-store/README.md b/samples/springboot3/alt-pet-store/README.md index d8cf8383d..b91cd42f6 100644 --- a/samples/springboot3/alt-pet-store/README.md +++ b/samples/springboot3/alt-pet-store/README.md @@ -36,4 +36,21 @@ PetStoreApi - URL for application https://xxxxxxxxxx.execute-api.us-w --------------------------------------------------------------------------------------------------------- $ curl https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/pets -``` \ No newline at end of file +``` + +You can also try a complex request passing both path and request parameters to complex endpoint such as this: + + +``` +@RequestMapping(path = "/foo/{gender}/bar/{age}", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) +public String complexRequest(@RequestBody String body, + @PathVariable("gender") String gender, + @PathVariable("age") String age, + @RequestParam("name") String name +) +``` +For example. + +``` +curl -d '{"key1":"value1", "key2":"value2"}' -H "Content-Type: application/json" -X POST https://zuhd709386.execute-api.us-east-2.amazonaws.com/foo/male/bar/25?name=Ricky +``` diff --git a/samples/springboot3/alt-pet-store/pom.xml b/samples/springboot3/alt-pet-store/pom.xml index afda4120e..0af236afe 100644 --- a/samples/springboot3/alt-pet-store/pom.xml +++ b/samples/springboot3/alt-pet-store/pom.xml @@ -39,6 +39,12 @@ aws-serverless-java-container-springboot3 2.0.0-SNAPSHOT + + + io.swagger.core.v3 + swagger-annotations + 2.2.20 + diff --git a/samples/springboot3/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot3/controller/PetsController.java b/samples/springboot3/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot3/controller/PetsController.java index 680e629d3..769db35f3 100644 --- a/samples/springboot3/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot3/controller/PetsController.java +++ b/samples/springboot3/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot3/controller/PetsController.java @@ -17,6 +17,8 @@ import com.amazonaws.serverless.sample.springboot3.model.Pet; import com.amazonaws.serverless.sample.springboot3.model.PetData; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -32,6 +34,7 @@ @RestController @EnableWebMvc public class PetsController { + @RequestMapping(path = "/pets", method = RequestMethod.POST) public Pet createPet(@RequestBody Pet newPet) { if (newPet.getName() == null || newPet.getBreed() == null) { @@ -73,5 +76,15 @@ public Pet listPets() { newPet.setName(PetData.getRandomName()); return newPet; } + + @RequestMapping(path = "/foo/{gender}/bar/{age}", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) + public String complexRequest(@RequestBody String body, + @PathVariable("gender") String gender, + @PathVariable("age") String age, + @RequestParam("name") String name + ) { + System.out.println("Body: " + body + " - " + gender + "/" + age + "/" + name); + return gender + "/" + age + "/" + name; + } } diff --git a/samples/springboot3/alt-pet-store/template.yml b/samples/springboot3/alt-pet-store/template.yml index c883f0850..8a51c8d1d 100644 --- a/samples/springboot3/alt-pet-store/template.yml +++ b/samples/springboot3/alt-pet-store/template.yml @@ -14,7 +14,7 @@ Resources: # AutoPublishAlias: bcn FunctionName: pet-store-boot-3 Handler: com.amazonaws.serverless.proxy.spring.SpringDelegatingLambdaContainerHandler::handleRequest - Runtime: java21 + Runtime: java17 SnapStart: ApplyOn: PublishedVersions CodeUri: .