Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.time.Duration;
import java.util.Base64;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
Expand Down Expand Up @@ -104,7 +105,7 @@ public Status execute(LightLambdaExchange exchange) {
if (!exchange.hasFailedState()) {
/* invoke lambda function */
var path = exchange.getRequest().getPath();
var method = exchange.getRequest().getHttpMethod().toLowerCase();
var method = normalizeMethod(exchange.getRequest().getHttpMethod());
LOG.debug("Request path: {} -- Request method: {} -- Start time: {}", path, method, System.currentTimeMillis());
var functionName = resolveFunctionName(path, method);
if (functionName == null) {
Expand Down Expand Up @@ -154,17 +155,32 @@ String resolveFunctionName(final String path, final String method) {
private void populateMethodToMatcherMap(final Map<String, String> functions) {
this.methodToMatcherMap.clear();
for (var entry : functions.entrySet()) {
var endpoint = entry.getKey().split("@");
var path = endpoint[0];
var method = endpoint[1];
var endpoint = entry.getKey();
var separatorIndex = endpoint.lastIndexOf('@');
if (separatorIndex < 1 || separatorIndex == endpoint.length() - 1) {
LOG.error("Skipping lambda-proxy function '{}': the key must use the 'path@method' format.", endpoint);
continue;
}
var path = endpoint.substring(0, separatorIndex);
var method = normalizeMethod(endpoint.substring(separatorIndex + 1));
PathTemplateMatcher<String> matcher = this.methodToMatcherMap.computeIfAbsent(method,
k -> new PathTemplateMatcher<>());
if (matcher.get(path) == null)
matcher.add(path, entry.getValue());
this.methodToMatcherMap.put(method, matcher);
}
}

/**
* Normalizes an HTTP method to lower case so that the route table built from the configuration
* and the lookup performed for an incoming request always agree on the key.
*
* @param method the HTTP method to normalize
* @return the method in lower case
*/
private static String normalizeMethod(final String method) {
return method.toLowerCase(Locale.ROOT);
}

private String invokeFunction(
final LambdaAsyncClient client,
final String functionName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.util.HashMap;
import java.util.Map;

/**
Expand Down Expand Up @@ -91,4 +92,57 @@ public void testExecuteReturnsFailureStatusWhenPathDoesNotMatch() {
() -> middleware.execute(exchangeFor("/v1/unknown", "GET")));
Assertions.assertEquals(LambdaProxyMiddleware.FAILED_TO_INVOKE_LAMBDA, status.getCode());
}

/**
* The route table is keyed by the configured method and looked up with the request method, so both
* sides must agree on case. An upper case configuration key used to register a route that no
* request could ever reach.
*/
@Test
public void testUpperCaseConfiguredMethodStillRoutes() {
var middleware = new LambdaProxyMiddleware(Map.of("/v1/pets@GET", "PetsGetFunction"));
Assertions.assertEquals("PetsGetFunction", middleware.resolveFunctionName("/v1/pets", "get"));
}

@Test
public void testMixedCaseConfiguredMethodStillRoutes() {
var middleware = new LambdaProxyMiddleware(Map.of("/v1/pets@Delete", "PetsDeleteFunction"));
Assertions.assertEquals("PetsDeleteFunction", middleware.resolveFunctionName("/v1/pets", "delete"));
}

/**
* The request method is upper case on the wire, so an upper case configuration key must be
* reachable through the same normalization {@code execute} applies before the lookup.
*/
@Test
public void testUpperCaseConfiguredMethodIsReachableByAnUpperCaseRequest() {
var middleware = new LambdaProxyMiddleware(Map.of("/v1/pets@POST", "PetsPostFunction"));
var exchange = exchangeFor("/v1/pets", "POST");
var method = exchange.getRequest().getHttpMethod().toLowerCase(java.util.Locale.ROOT);
Assertions.assertEquals("PetsPostFunction",
middleware.resolveFunctionName(exchange.getRequest().getPath(), method));
}

/**
* A path containing '@' must still split on the separator before the method rather than the first
* '@' in the key.
*/
@Test
public void testPathContainingSeparatorIsSplitOnTheLastSeparator() {
var middleware = new LambdaProxyMiddleware(Map.of("/v1/users/{user@domain}@get", "UserGetFunction"));
Assertions.assertEquals("UserGetFunction", middleware.resolveFunctionName("/v1/users/a@b.com", "get"));
}

@Test
public void testMalformedConfigurationKeysAreSkippedWithoutFailingStartup() {
var functions = new HashMap<String, String>();
functions.put("/v1/pets", "MissingMethodFunction");
functions.put("@get", "MissingPathFunction");
functions.put("/v1/pets@", "EmptyMethodFunction");
functions.put("/v1/pets@get", "PetsGetFunction");

var middleware = Assertions.assertDoesNotThrow(() -> new LambdaProxyMiddleware(functions));
Assertions.assertEquals("PetsGetFunction", middleware.resolveFunctionName("/v1/pets", "get"),
"a malformed entry must not prevent the valid entries from being registered");
}
}