diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/AwsProxyExceptionHandler.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/AwsProxyExceptionHandler.java index 9280a5b48..e515353b5 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/AwsProxyExceptionHandler.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/AwsProxyExceptionHandler.java @@ -13,11 +13,14 @@ package com.amazonaws.serverless.proxy; import com.amazonaws.serverless.exceptions.InvalidRequestEventException; +import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; import com.amazonaws.serverless.proxy.model.AwsProxyResponse; import com.amazonaws.serverless.proxy.model.ErrorModel; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonValueFormat; +import com.fasterxml.jackson.databind.ser.std.JsonValueSerializer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -55,8 +58,6 @@ public class AwsProxyExceptionHandler //------------------------------------------------------------- private static Map headers = new HashMap<>(); - private static ObjectMapper objectMapper = new ObjectMapper(); - //------------------------------------------------------------- // Constructors @@ -87,7 +88,7 @@ public AwsProxyResponse handle(Throwable ex) { public void handle(Throwable ex, OutputStream stream) throws IOException { AwsProxyResponse response = handle(ex); - objectMapper.writeValue(stream, response); + LambdaContainerHandler.getObjectMapper().writeValue(stream, response); } @@ -96,8 +97,9 @@ public void handle(Throwable ex, OutputStream stream) throws IOException { //------------------------------------------------------------- String getErrorJson(String message) { + try { - return objectMapper.writeValueAsString(new ErrorModel(message)); + return LambdaContainerHandler.getObjectMapper().writeValueAsString(new ErrorModel(message)); } catch (JsonProcessingException e) { log.error("Could not produce error JSON", e); return "{ \"message\": \"" + message + "\" }"; diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/LambdaContainerHandler.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/LambdaContainerHandler.java index 6dfa30697..260037422 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/LambdaContainerHandler.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/LambdaContainerHandler.java @@ -20,6 +20,7 @@ import com.amazonaws.serverless.proxy.SecurityContextWriter; import com.amazonaws.services.lambda.runtime.Context; +import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -64,6 +65,8 @@ public abstract class LambdaContainerHandler> urlEncodedFormParameters; private Map multipartFormParameters; private Logger log = LoggerFactory.getLogger(AwsProxyHttpServletRequest.class); + private ContainerConfig config; //------------------------------------------------------------- @@ -89,6 +91,17 @@ public AwsProxyHttpServletRequest(AwsProxyRequest awsProxyRequest, Context lambd super(lambdaContext); this.request = awsProxyRequest; this.securityContext = awsSecurityContext; + this.config = ContainerConfig.defaultConfig(); + + this.urlEncodedFormParameters = getFormUrlEncodedParametersMap(); + this.multipartFormParameters = getMultipartFormParametersMap(); + } + + public AwsProxyHttpServletRequest(AwsProxyRequest awsProxyRequest, Context lambdaContext, SecurityContext awsSecurityContext, ContainerConfig config) { + super(lambdaContext); + this.request = awsProxyRequest; + this.securityContext = awsSecurityContext; + this.config = config; this.urlEncodedFormParameters = getFormUrlEncodedParametersMap(); this.multipartFormParameters = getMultipartFormParametersMap(); @@ -181,10 +194,7 @@ public String getMethod() { @Override public String getPathInfo() { - String pathInfo = getServletPath().replace(getContextPath(), ""); - if (!pathInfo.startsWith("/")) { - pathInfo = "/" + pathInfo; - } + String pathInfo = cleanUri(request.getPath()); //getServletPath().replace(getContextPath(), ""); return decodeRequestPath(pathInfo, LambdaContainerHandler.getContainerConfig()); } @@ -196,13 +206,18 @@ public String getPathTranslated() { } - /** - * In AWS API Gateway, stage is never given as part of the path. - * @return - */ @Override public String getContextPath() { - return ""; + if (config.isUseStageAsServletContext()) { + String contextPath = cleanUri(request.getRequestContext().getStage()); + if (config.getServiceBasePath() != null) { + contextPath += cleanUri(config.getServiceBasePath()); + } + + return contextPath; + } else { + return "" + (config.getServiceBasePath() != null ? cleanUri(config.getServiceBasePath()) : ""); + } } @@ -232,7 +247,7 @@ public Principal getUserPrincipal() { @Override public String getRequestURI() { - return (getContextPath().isEmpty() ? "" : "/" + getContextPath()) + request.getPath(); + return cleanUri(getContextPath()) + cleanUri(request.getPath()); } @@ -240,12 +255,8 @@ public String getRequestURI() { public StringBuffer getRequestURL() { String url = ""; url += getServerName(); - url += "/"; - url += getContextPath(); - url += "/"; - url += request.getPath(); - - url = url.replaceAll("/+", "/"); + url += cleanUri(getContextPath()); + url += cleanUri(request.getPath()); return new StringBuffer(getScheme() + "://" + url); } @@ -253,7 +264,8 @@ public StringBuffer getRequestURL() { @Override public String getServletPath() { - return decodeRequestPath(request.getPath(), LambdaContainerHandler.getContainerConfig()); + // we always work on the root path + return ""; } @Override @@ -653,15 +665,15 @@ private String getQueryStringParameterCaseInsensitive(String key) { private String[] getFormBodyParameterCaseInsensitive(String key) { - List values = urlEncodedFormParameters.get(key); - if (values != null) { - String[] valuesArray = new String[values.size()]; - valuesArray = values.toArray(valuesArray); - return valuesArray; - } else { - return null; - } + List values = urlEncodedFormParameters.get(key); + if (values != null) { + String[] valuesArray = new String[values.size()]; + valuesArray = values.toArray(valuesArray); + return valuesArray; + } else { + return null; } + } private Map getMultipartFormParametersMap() { @@ -698,6 +710,22 @@ private Map getMultipartFormParametersMap() { return output; } + private String cleanUri(String uri) { + String finalUri = uri; + + if (!finalUri.startsWith("/")) { + finalUri = "/" + finalUri; + } + + if (finalUri.endsWith(("/"))) { + finalUri = finalUri.substring(0, finalUri.length() - 1); + } + + finalUri = finalUri.replaceAll("/+", "/"); + + return finalUri; + } + private Map> getFormUrlEncodedParametersMap() { String contentType = getContentType(); diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletRequestReader.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletRequestReader.java index 25a2bd7d4..949ed7e35 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletRequestReader.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletRequestReader.java @@ -34,7 +34,7 @@ public class AwsProxyHttpServletRequestReader extends RequestReaderFilterChainHolder object that can be used to apply the filters to the request */ FilterChainHolder getFilterChain(final HttpServletRequest request, Servlet servlet) { - String targetPath = request.getServletPath(); + String targetPath = request.getRequestURI(); DispatcherType type = request.getDispatcherType(); // only return the cached result if the filter list hasn't changed in the meanwhile diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/testutils/AwsProxyRequestBuilder.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/testutils/AwsProxyRequestBuilder.java index c393e4081..54bd0ebe3 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/testutils/AwsProxyRequestBuilder.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/testutils/AwsProxyRequestBuilder.java @@ -12,6 +12,7 @@ */ package com.amazonaws.serverless.proxy.internal.testutils; +import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; import com.amazonaws.serverless.proxy.model.ApiGatewayAuthorizerContext; import com.amazonaws.serverless.proxy.model.ApiGatewayRequestContext; import com.amazonaws.serverless.proxy.model.ApiGatewayRequestIdentity; @@ -41,8 +42,6 @@ public class AwsProxyRequestBuilder { //------------------------------------------------------------- private AwsProxyRequest request; - private ObjectMapper mapper; - //------------------------------------------------------------- // Constructors @@ -59,7 +58,6 @@ public AwsProxyRequestBuilder(String path) { public AwsProxyRequestBuilder(String path, String httpMethod) { - this.mapper = new ObjectMapper(); this.request = new AwsProxyRequest(); this.request.setHeaders(new HashMap<>()); // avoid NPE @@ -144,7 +142,7 @@ public AwsProxyRequestBuilder body(String body) { public AwsProxyRequestBuilder body(Object body) { if (request.getHeaders() != null && request.getHeaders().get(HttpHeaders.CONTENT_TYPE).equals(MediaType.APPLICATION_JSON)) { try { - return body(mapper.writeValueAsString(body)); + return body(LambdaContainerHandler.getObjectMapper().writeValueAsString(body)); } catch (JsonProcessingException e) { throw new UnsupportedOperationException("Could not serialize object: " + e.getMessage()); } @@ -239,13 +237,13 @@ public AwsProxyRequestBuilder serverName(String serverName) { public AwsProxyRequestBuilder fromJsonString(String jsonContent) throws IOException { - request = mapper.readValue(jsonContent, AwsProxyRequest.class); + request = LambdaContainerHandler.getObjectMapper().readValue(jsonContent, AwsProxyRequest.class); return this; } public AwsProxyRequestBuilder fromJsonPath(String filePath) throws IOException { - request = mapper.readValue(new File(filePath), AwsProxyRequest.class); + request = LambdaContainerHandler.getObjectMapper().readValue(new File(filePath), AwsProxyRequest.class); return this; } diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/ContainerConfig.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/ContainerConfig.java index 2a0197096..668f25e01 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/ContainerConfig.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/ContainerConfig.java @@ -1,6 +1,9 @@ package com.amazonaws.serverless.proxy.model; +import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletRequest; + + /** * Configuration parameters for the framework */ @@ -12,6 +15,7 @@ public static ContainerConfig defaultConfig() { configuration.setStripBasePath(false); configuration.setUriEncoding(DEFAULT_URI_ENCODING); configuration.setConsolidateSetCookieHeaders(true); + configuration.setUseStageAsServletContext(false); return configuration; } @@ -24,19 +28,29 @@ public static ContainerConfig defaultConfig() { private boolean stripBasePath; private String uriEncoding; private boolean consolidateSetCookieHeaders; + private boolean useStageAsServletContext; //------------------------------------------------------------- // Methods - Getter/Setter //------------------------------------------------------------- + + /** + * Returns the base path configured in the container. This configuration variable is used in conjuction with {@link #setStripBasePath(boolean)} to route + * the request. When requesting the context path from an HttpServletRequest: {@link AwsProxyHttpServletRequest#getContextPath()} this base path is added + * to the context even though it was initially stripped for the purpose of routing the request. We decided to add it to the context to address GitHub issue + * #84 and allow framework's link builders to it. + * + * @return The base path configured for the container + */ public String getServiceBasePath() { return serviceBasePath; } /** - * Configures a base path that can be strippped from the request path before passing it to the frameowkr-specific implementation. This can be used to + * Configures a base path that can be stripped from the request path before passing it to the framework-specific implementation. This can be used to * remove API Gateway's base path mappings from the request. * @param serviceBasePath The base path mapping to be removed. */ @@ -99,4 +113,22 @@ public boolean isConsolidateSetCookieHeaders() { public void setConsolidateSetCookieHeaders(boolean consolidateSetCookieHeaders) { this.consolidateSetCookieHeaders = consolidateSetCookieHeaders; } + + + /** + * Tells whether the stage name passed in the request should be added to the context path: {@link AwsProxyHttpServletRequest#getContextPath()}. + * @return true if the stage will be included in the context path, false otherwise. + */ + public boolean isUseStageAsServletContext() { + return useStageAsServletContext; + } + + + /** + * Sets whether the API Gateway stage name should be included in the servlet context path. + * @param useStageAsServletContext true if you want the stage to appear as the root of the context path, false otherwise. + */ + public void setUseStageAsServletContext(boolean useStageAsServletContext) { + this.useStageAsServletContext = useStageAsServletContext; + } } diff --git a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpServletRequestTest.java b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpServletRequestTest.java index e10526f7d..70106e0a0 100644 --- a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpServletRequestTest.java +++ b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpServletRequestTest.java @@ -3,6 +3,8 @@ import com.amazonaws.serverless.proxy.model.AwsProxyRequest; import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext; +import com.amazonaws.serverless.proxy.model.ContainerConfig; + import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.Test; @@ -28,9 +30,11 @@ public class AwsHttpServletRequestTest { private static final MockLambdaContext mockContext = new MockLambdaContext(); + private static ContainerConfig config = ContainerConfig.defaultConfig(); + @Test public void headers_parseHeaderValue_multiValue() { - AwsProxyHttpServletRequest request = new AwsProxyHttpServletRequest(contentTypeRequest, mockContext, null); + AwsProxyHttpServletRequest request = new AwsProxyHttpServletRequest(contentTypeRequest, mockContext, null, config); // I'm also using this to double-check that I can get a header ignoring case List> values = request.parseHeaderValue(request.getHeader("content-type")); @@ -44,7 +48,7 @@ public void headers_parseHeaderValue_multiValue() { @Test public void headers_parseHeaderValue_validMultipleCookie() { - AwsProxyHttpServletRequest request = new AwsProxyHttpServletRequest(validCookieRequest, mockContext, null); + AwsProxyHttpServletRequest request = new AwsProxyHttpServletRequest(validCookieRequest, mockContext, null, config); List> values = request.parseHeaderValue(request.getHeader(HttpHeaders.COOKIE)); assertEquals(2, values.size()); @@ -56,7 +60,7 @@ public void headers_parseHeaderValue_validMultipleCookie() { @Test public void headers_parseHeaderValue_complexAccept() { - AwsProxyHttpServletRequest request = new AwsProxyHttpServletRequest(complexAcceptHeader, mockContext, null); + AwsProxyHttpServletRequest request = new AwsProxyHttpServletRequest(complexAcceptHeader, mockContext, null, config); List> values = request.parseHeaderValue(request.getHeader(HttpHeaders.ACCEPT)); try { @@ -69,7 +73,7 @@ public void headers_parseHeaderValue_complexAccept() { @Test public void queyrString_generateQueryString_validQuery() { - AwsProxyHttpServletRequest request = new AwsProxyHttpServletRequest(queryString, mockContext, null); + AwsProxyHttpServletRequest request = new AwsProxyHttpServletRequest(queryString, mockContext, null, config); String parsedString = request.generateQueryString(queryString.getQueryStringParameters()); assertEquals("one=two&three=four", parsedString); diff --git a/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/jersey/EchoJerseyResource.java b/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/jersey/EchoJerseyResource.java index 4a512455a..e3a89b8df 100644 --- a/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/jersey/EchoJerseyResource.java +++ b/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/jersey/EchoJerseyResource.java @@ -34,6 +34,8 @@ @Path("/echo") public class EchoJerseyResource { + public static final String EXCEPTION_MESSAGE = "Fake exception"; + @Path("/headers") @GET @Produces(MediaType.APPLICATION_JSON) public MapResponseModel echoHeaders(@Context ContainerRequestContext context) { @@ -127,4 +129,9 @@ public Response echoBinaryData() { return Response.ok(b).build(); } + + @Path("/exception") @GET + public Response throwException() { + throw new UnsupportedOperationException(EXCEPTION_MESSAGE); + } } diff --git a/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/jersey/JerseyAwsProxyTest.java b/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/jersey/JerseyAwsProxyTest.java index 9e1d11cc0..36031996b 100644 --- a/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/jersey/JerseyAwsProxyTest.java +++ b/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/jersey/JerseyAwsProxyTest.java @@ -34,6 +34,7 @@ import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.Response; import java.io.IOException; import java.util.UUID; @@ -53,7 +54,7 @@ public class JerseyAwsProxyTest { private static ObjectMapper objectMapper = new ObjectMapper(); - private static ResourceConfig app = new ResourceConfig().packages("com.amazonaws.serverless.proxy.jersey") + private static ResourceConfig app = new ResourceConfig().packages("com.amazonaws.serverless.proxy.jersey", "com.amazonaws.serverless.proxy.jersey.providers") .register(new AbstractBinder() { @Override protected void configure() { @@ -245,6 +246,16 @@ public void base64_binaryResponse_base64Encoding() { assertTrue(Base64.isBase64(response.getBody())); } + @Test + public void exception_mapException_mapToNotImplemented() { + AwsProxyRequest request = new AwsProxyRequestBuilder("/echo/exception", "GET").build(); + + AwsProxyResponse response = handler.proxy(request, lambdaContext); + assertNotNull(response.getBody()); + assertEquals(EchoJerseyResource.EXCEPTION_MESSAGE, response.getBody()); + assertEquals(Response.Status.NOT_IMPLEMENTED.getStatusCode(), response.getStatusCode()); + } + private void validateMapResponseModel(AwsProxyResponse output) { validateMapResponseModel(output, CUSTOM_HEADER_KEY, CUSTOM_HEADER_VALUE); } diff --git a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/SpringAwsProxyTest.java b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/SpringAwsProxyTest.java index 0c26a61ba..1fa4d666e 100644 --- a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/SpringAwsProxyTest.java +++ b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/SpringAwsProxyTest.java @@ -5,6 +5,7 @@ import com.amazonaws.serverless.proxy.internal.servlet.AwsServletContext; import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext; +import com.amazonaws.serverless.proxy.spring.echoapp.EchoResource; import com.amazonaws.serverless.proxy.spring.echoapp.EchoSpringAppConfig; import com.amazonaws.serverless.proxy.spring.echoapp.UnauthenticatedFilter; import com.amazonaws.serverless.proxy.spring.echoapp.model.MapResponseModel; @@ -277,6 +278,26 @@ public void request_encodedPath_returnsDecodedPath() { } + @Test + public void contextPath_generateLink_returnsCorrectPath() { + AwsProxyRequest request = new AwsProxyRequestBuilder("/echo/generate-uri", "GET") + .scheme("https") + .serverName("api.myserver.com") + .stage("prod") + .build(); + SpringLambdaContainerHandler.getContainerConfig().setUseStageAsServletContext(true); + + AwsProxyResponse output = handler.proxy(request, lambdaContext); + assertEquals(200, output.getStatusCode()); + System.out.println("Response: " + output.getBody()); + + String expectedUri = "https://api.myserver.com/prod/echo/encoded-request-uri/" + EchoResource.TEST_GENERATE_URI; + + validateSingleValueModel(output, expectedUri); + + SpringLambdaContainerHandler.getContainerConfig().setUseStageAsServletContext(false); + } + private void validateMapResponseModel(AwsProxyResponse output) { try { MapResponseModel response = objectMapper.readValue(output.getBody(), MapResponseModel.class); diff --git a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/echoapp/EchoResource.java b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/echoapp/EchoResource.java index bae0e045d..688e77604 100644 --- a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/echoapp/EchoResource.java +++ b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/echoapp/EchoResource.java @@ -12,14 +12,22 @@ import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; + +import java.net.URI; import java.util.Enumeration; import java.util.Map; import java.util.Random; +import static org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder.fromMethodCall; +import static org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder.on; + + @RestController @EnableWebMvc @RequestMapping("/echo") public class EchoResource { + public static final String TEST_GENERATE_URI = "test"; + @Autowired ServletContext servletContext; @@ -129,4 +137,15 @@ public SingleValueModel echoEncodedRequestUri(@PathVariable("encoded-var") Strin return valueModel; } + @RequestMapping(path = "/generate-uri", method = RequestMethod.GET) + public SingleValueModel echoGeneratedResourceLink() { + SingleValueModel valueModel = new SingleValueModel(); + + URI personUri = fromMethodCall(on(EchoResource.class).echoEncodedRequestUri(TEST_GENERATE_URI)).build().toUri(); + + valueModel.setValue(personUri.toString()); + + return valueModel; + } + }