Repository: localstack/localstack

Issue:
Support "ANY" HTTP method in API Gateway resources
Hey,

Currently it is not possible to call an API Gateway which has a resource with its HTTP method set to `ANY`. It is however possible to create such a resource in localstack. The  implemented behavior seems to be that localstack actually expects the HTTP method of incoming requests to be `ANY` instead of interpreting `ANY` as "any HTTP methods".

 I quickly investigated and I believe the fix may be done either in [get_apigateway_integration](https://github.com/localstack/localstack/blob/master/localstack/utils/aws/aws_stack.py#L321-L335) or directly in [forward_request](https://github.com/localstack/localstack/blob/master/localstack/services/apigateway/apigateway_listener.py#L157-L169).

Thanks.


Discussion:
Thanks for reporting @Luxoboy - indeed [forward_request](https://github.com/localstack/localstack/blob/master/localstack/services/apigateway/apigateway_listener.py#L157-L169) would be a good starting point. Any chance you could create a small pull request for this feature (including a few tests)? I'd be happy to help out with the implementation, if there are any questions. Thanks!
I'm trying to setup localstack on my machine (OSX) to give this issue a shot, but I cannot invoke a lambda via API gateway. I keep getting this error:
```
Error on request:
Traceback (most recent call last):
File "/Users/cryptic/dev/localstack/.venv/lib/python2.7/site-packages/werkzeug/serving.py", line 270, in run_wsgi
execute(self.server.app)
File "/Users/cryptic/dev/localstack/.venv/lib/python2.7/site-packages/werkzeug/serving.py", line 258, in execute
application_iter = app(environ, start_response)
File "/Users/cryptic/dev/localstack/.venv/lib/python2.7/site-packages/moto/server.py", line 93, in __call__
return backend_app(environ, start_response)
File "/Users/cryptic/dev/localstack/.venv/lib/python2.7/site-packages/flask/app.py", line 1836, in __call__
return self.wsgi_app(environ, start_response)
File "/Users/cryptic/dev/localstack/.venv/lib/python2.7/site-packages/flask/app.py", line 1820, in wsgi_app
response = self.make_response(self.handle_exception(e))
File "/Users/cryptic/dev/localstack/.venv/lib/python2.7/site-packages/flask/app.py", line 1403, in handle_exception
reraise(exc_type, exc_value, tb)
File "/Users/cryptic/dev/localstack/.venv/lib/python2.7/site-packages/flask/app.py", line 1817, in wsgi_app
response = self.full_dispatch_request()
File "/Users/cryptic/dev/localstack/.venv/lib/python2.7/site-packages/flask/app.py", line 1477, in full_dispatch_request
rv = self.handle_user_exception(e)
File "/Users/cryptic/dev/localstack/.venv/lib/python2.7/site-packages/flask/app.py", line 1381, in handle_user_exception
reraise(exc_type, exc_value, tb)
File "/Users/cryptic/dev/localstack/.venv/lib/python2.7/site-packages/flask/app.py", line 1475, in full_dispatch_request
rv = self.dispatch_request()
File "/Users/cryptic/dev/localstack/.venv/lib/python2.7/site-packages/flask/app.py", line 1461, in dispatch_request
return self.view_functions[rule.endpoint](**req.view_args)
File "/Users/cryptic/dev/localstack/.venv/lib/python2.7/site-packages/moto/core/utils.py", line 139, in __call__
result = self.callback(request, request.url, {})
File "/Users/cryptic/dev/localstack/.venv/lib/python2.7/site-packages/moto/apigateway/responses.py", line 57, in resources
resources = self.backend.list_resources(function_id)
File "/Users/cryptic/dev/localstack/.venv/lib/python2.7/site-packages/moto/apigateway/models.py", line 491, in list_resources
api = self.get_rest_api(function_id)
File "/Users/cryptic/dev/localstack/.venv/lib/python2.7/site-packages/moto/apigateway/models.py", line 480, in get_rest_api
rest_api = self.apis[function_id]
KeyError: u'3574566326'
```

It works fine from the docker image though.

I put how I came up with the lambda and the API gateway integration in [this gist](https://gist.github.com/crypticmind/c75db15fd774fe8f53282c3ccbe3d7ad).

**EDIT**: The error above was caused by not having `us-east-1` as the default region.


Resolving patch:
diff --git a/localstack/services/apigateway/apigateway_listener.py b/localstack/services/apigateway/apigateway_listener.py
--- a/localstack/services/apigateway/apigateway_listener.py
+++ b/localstack/services/apigateway/apigateway_listener.py
@@ -168,23 +168,29 @@ def forward_request(self, method, path, data, headers):
                 integration = aws_stack.get_apigateway_integration(api_id, method, path=relative_path)
                 assert integration
             except Exception:
-                # if we have no exact match, try to find an API resource that contains path parameters
-                path_map = get_rest_api_paths(rest_api_id=api_id)
                 try:
-                    extracted_path, resource = get_resource_for_path(path=relative_path, path_map=path_map)
+                    integration = aws_stack.get_apigateway_integration(api_id, 'ANY', path=relative_path)
+                    assert integration
                 except Exception:
-                    return make_error('Unable to find path %s' % path, 404)
+                    # if we have no exact match, try to find an API resource that contains path parameters
+                    path_map = get_rest_api_paths(rest_api_id=api_id)
+                    try:
+                        extracted_path, resource = get_resource_for_path(path=relative_path, path_map=path_map)
+                    except Exception:
+                        return make_error('Unable to find path %s' % path, 404)
 
-                integrations = resource.get('resourceMethods', {})
-                integration = integrations.get(method, {})
-                integration = integration.get('methodIntegration')
-                if not integration:
+                    integrations = resource.get('resourceMethods', {})
+                    integration = integrations.get(method, {})
+                    if not integration:
+                        integration = integrations.get('ANY', {})
+                    integration = integration.get('methodIntegration')
+                    if not integration:
 
-                    if method == 'OPTIONS' and 'Origin' in headers:
-                        # default to returning CORS headers if this is an OPTIONS request
-                        return get_cors_response(headers)
+                        if method == 'OPTIONS' and 'Origin' in headers:
+                            # default to returning CORS headers if this is an OPTIONS request
+                            return get_cors_response(headers)
 
-                    return make_error('Unable to find integration for path %s' % path, 404)
+                        return make_error('Unable to find integration for path %s' % path, 404)
 
             uri = integration.get('uri')
             if method == 'POST' and integration['type'] == 'AWS':
diff --git a/localstack/utils/aws/aws_stack.py b/localstack/utils/aws/aws_stack.py
--- a/localstack/utils/aws/aws_stack.py
+++ b/localstack/utils/aws/aws_stack.py
@@ -451,6 +451,10 @@ def create_api_gateway_integrations(api_id, resource_id, method, integrations=[]
             )
 
 
+def apigateway_invocations_arn(lambda_uri):
+    return 'arn:aws:apigateway:%s:lambda:path/2015-03-31/functions/%s/invocations' % (DEFAULT_REGION, lambda_uri)
+
+
 def get_elasticsearch_endpoint(domain=None, region_name=None):
     env = get_environment(region_name=region_name)
     if env.region == REGION_LOCAL:


Test patch:
diff --git a/tests/integration/lambdas/lambda_integration.py b/tests/integration/lambdas/lambda_integration.py
--- a/tests/integration/lambdas/lambda_integration.py
+++ b/tests/integration/lambdas/lambda_integration.py
@@ -26,8 +26,12 @@ def handler(event, context):
 
     if 'httpMethod' in event:
         # looks like this is a call from an AWS_PROXY API Gateway
-        body = json.loads(event['body'])
+        try:
+            body = json.loads(event['body'])
+        except Exception:
+            body = {}
         body['pathParameters'] = event.get('pathParameters')
+        body['httpMethod'] = event.get('httpMethod')
         return {
             'body': body,
             'statusCode': body.get('return_status_code', 200),
diff --git a/tests/integration/test_api_gateway.py b/tests/integration/test_api_gateway.py
--- a/tests/integration/test_api_gateway.py
+++ b/tests/integration/test_api_gateway.py
@@ -34,10 +34,12 @@
 API_PATH_DATA_INBOUND = '/data'
 API_PATH_HTTP_BACKEND = '/hello_world'
 API_PATH_LAMBDA_PROXY_BACKEND = '/lambda/{test_param1}'
+API_PATH_LAMBDA_PROXY_BACKEND_ANY_METHOD = '/lambda-any-method/{test_param1}'
 # name of Kinesis stream connected to API Gateway
 TEST_STREAM_KINESIS_API_GW = 'test-stream-api-gw'
 TEST_STAGE_NAME = 'testing'
 TEST_LAMBDA_PROXY_BACKEND = 'test_lambda_apigw_backend'
+TEST_LAMBDA_PROXY_BACKEND_ANY_METHOD = 'test_lambda_apigw_backend_any_method'
 
 
 def connect_api_gateway_to_kinesis(gateway_name, kinesis_stream):
@@ -187,3 +189,33 @@ def test_api_gateway_lambda_proxy_integration():
     assert parsed_body.get('return_status_code') == 203
     assert parsed_body.get('return_headers') == {'foo': 'bar123'}
     assert parsed_body.get('pathParameters') == {'test_param1': 'foo1'}
+    result = requests.delete(url, data=json.dumps(data))
+    assert result.status_code == 404
+
+
+def test_api_gateway_lambda_proxy_integration_any_method():
+    # create lambda function
+    zip_file = testutil.create_lambda_archive(load_file(TEST_LAMBDA_PYTHON), get_content=True,
+        libs=TEST_LAMBDA_LIBS, runtime=LAMBDA_RUNTIME_PYTHON27)
+    testutil.create_lambda_function(func_name=TEST_LAMBDA_PROXY_BACKEND_ANY_METHOD,
+        zip_file=zip_file, runtime=LAMBDA_RUNTIME_PYTHON27)
+
+    # create API Gateway and connect it to the Lambda proxy backend
+    lambda_uri = aws_stack.lambda_function_arn(TEST_LAMBDA_PROXY_BACKEND_ANY_METHOD)
+    target_uri = aws_stack.apigateway_invocations_arn(lambda_uri)
+
+    result = connect_api_gateway_to_http_with_lambda_proxy('test_gateway3', target_uri,
+        methods=['ANY'],
+        path=API_PATH_LAMBDA_PROXY_BACKEND_ANY_METHOD)
+
+    # make test request to gateway and check response
+    path = API_PATH_LAMBDA_PROXY_BACKEND_ANY_METHOD.replace('{test_param1}', 'foo1')
+    url = INBOUND_GATEWAY_URL_PATTERN.format(api_id=result['id'], stage_name=TEST_STAGE_NAME, path=path)
+    data = {}
+
+    for method in ('GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'):
+        body = json.dumps(data) if method in ('POST', 'PUT', 'PATCH') else None
+        result = getattr(requests, method.lower())(url, data=body)
+        assert result.status_code == 200
+        parsed_body = json.loads(to_str(result.content))
+        assert parsed_body.get('httpMethod') == method