Problem:
When a request comes in to AWS Api Gateway, it finds the stage
and
forward the request to it. Then, the stage finds the route and calls
its target. In this post, we will explore how the stage selects the
route.
According to AWS API Gateway documentation, https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-routes.html , selecting routes happens with this priority:
- Full match for a route and method.
- Match for a route and method with a greedy path variable ({proxy+}).
- The $default route.
Let's implement this logic.
Solution
The idea is simple, we split the paths by /
and compare the segments.
route_definition = '/product/items/'.split('/')
incoming_route = '/product/items/'.split('/')
# compare segmants somehow
# route_definition[0] == incoming_route[0]
And here is the code
def select_route(route_keys, request_method, request_path):
return (
find_with_exact_match(route_keys, request_method, request_path) or
find_with_greedy_match(route_keys, request_method, request_path) or
'$default'
)
def find_with_exact_match(route_keys, request_method, request_path):
route = f'{request_method} {request_path}'
if route in route_keys:
return route
request_path_parts = request_path.split('/')
len_request_path_parts = len(request_path_parts)
possible_matches = []
for route_key in route_keys:
method, path = route_key.split(' ')
key_path_parts = path.split('/')
if len_request_path_parts != len(key_path_parts):
continue
if ((is_possible_match(request_path_parts, key_path_parts)) and
(method == request_method or method == 'ANY')):
return route_key
def find_with_greedy_match(route_keys, request_method, request_path):
greedy_route_keys = [_ for _ in route_keys if is_greedy_variable(_)]
request_path_parts = request_path.split('/')
len_request_path_parts = len(request_path_parts)
for route_key in greedy_route_keys:
method, path = route_key.split(' ')
key_path_parts = path.split('/')
if ((is_possible_greedy_match(request_path_parts, key_path_parts)) and
(method == request_method or method == 'ANY')):
return route_key
def is_possible_match(request_path_parts, key_path_parts):
for index, _ in enumerate(key_path_parts):
if is_variable(key_path_parts[index]):
continue
if key_path_parts[index] != request_path_parts[index]:
return False
return True
def is_possible_greedy_match(request_path_parts, key_path_parts):
for index, _ in enumerate(key_path_parts):
if is_greedy_variable(key_path_parts[index]):
return True
if key_path_parts[index] != request_path_parts[index]:
return False
return False
def is_variable(part):
return part.startswith('{') and part.endswith('}')
def is_greedy_variable(part):
return part.startswith('{') and part.endswith('+}')
Tests
from select_route import select_route
route_keys = [
'GET /product/items/1',
'GET /product/items/{id}',
'ANY /product/items/{id}/details',
'GET /product/{items+}',
'ANY /product/{items+}',
]
def test_select_route():
assert 'GET /product/items/1' == select_route(route_keys, 'GET', '/product/items/1')
assert 'GET /product/items/{id}' == select_route(route_keys, 'GET', '/product/items/123')
assert 'ANY /product/items/{id}/details' == select_route(route_keys, 'GET', '/product/items/1/details')
assert 'ANY /product/items/{id}/details' == select_route(route_keys, 'POST', '/product/items/123/details')
assert 'GET /product/{items+}' == select_route(route_keys, 'GET', '/product/loaded')
assert 'ANY /product/{items+}' == select_route(route_keys, 'POST', '/product/loaded')
assert '$default' == select_route(route_keys, 'GET', '/nothing')
if __name__ == '__main__':
test_select_route()