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:

  1. Full match for a route and method.
  2. Match for a route and method with a greedy path variable ({proxy+}).
  3. 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()