Skip to content

Commit

Permalink
fix apigw http proxy response passthrough (#10583)
Browse files Browse the repository at this point in the history
  • Loading branch information
cloutierMat committed Apr 2, 2024
1 parent a38de24 commit 85486d2
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 6 deletions.
5 changes: 3 additions & 2 deletions localstack/services/apigateway/integration.py
Expand Up @@ -802,9 +802,10 @@ def invoke(self, invocation_context: ApiInvocationContext):
uri,
result.status_code,
)
# apply custom response template
# apply custom response template for non-proxy integration
invocation_context.response = result
self.response_templates.render(invocation_context)
if integration["type"] != "HTTP_PROXY":
self.response_templates.render(invocation_context)
return invocation_context.response


Expand Down
35 changes: 34 additions & 1 deletion tests/aws/services/apigateway/conftest.py
Expand Up @@ -13,6 +13,7 @@
delete_rest_api,
import_rest_api,
)
from tests.aws.services.lambda_.test_lambda import TEST_LAMBDA_PYTHON_ECHO_STATUS_CODE

# default name used for created REST API stages
DEFAULT_STAGE_NAME = "dev"
Expand Down Expand Up @@ -69,6 +70,8 @@ def create_rest_api_with_integration(
):
def _factory(
integration_uri,
path_part="test",
req_parameters=None,
req_templates=None,
res_templates=None,
integration_type=None,
Expand All @@ -80,16 +83,20 @@ def _factory(
)

resource_id, _ = create_rest_resource(
aws_client.apigateway, restApiId=api_id, parentId=root_id, pathPart="test"
aws_client.apigateway, restApiId=api_id, parentId=root_id, pathPart=path_part
)

if req_parameters is None:
req_parameters = {}

method, _ = create_rest_resource_method(
aws_client.apigateway,
restApiId=api_id,
resourceId=resource_id,
httpMethod="POST",
authorizationType="NONE",
apiKeyRequired=False,
requestParameters={value: True for value in req_parameters.values()},
)

# set AWS policy to give API GW access to backend resources
Expand Down Expand Up @@ -120,6 +127,7 @@ def _factory(
credentials=assume_role_arn,
uri=integration_uri,
requestTemplates=req_templates or {},
requestParameters=req_parameters,
)

create_rest_api_method_response(
Expand Down Expand Up @@ -150,6 +158,31 @@ def _factory(
yield _factory


@pytest.fixture
def create_status_code_echo_server(aws_client, create_lambda_function):
lambda_client = aws_client.lambda_

def _create_status_code_echo_server():
function_name = f"lambda_fn_echo_status_{short_uid()}"
create_lambda_function(
func_name=function_name,
handler_file=TEST_LAMBDA_PYTHON_ECHO_STATUS_CODE,
)
create_url_response = lambda_client.create_function_url_config(
FunctionName=function_name, AuthType="NONE", InvokeMode="BUFFERED"
)
aws_client.lambda_.add_permission(
FunctionName=function_name,
StatementId="urlPermission",
Action="lambda:InvokeFunctionUrl",
Principal="*",
FunctionUrlAuthType="NONE",
)
return create_url_response["FunctionUrl"]

return _create_status_code_echo_server


@pytest.fixture
def apigw_redeploy_api(aws_client):
def _factory(rest_api_id: str, stage_name: str):
Expand Down
45 changes: 45 additions & 0 deletions tests/aws/services/apigateway/test_apigateway_http.py
@@ -1,4 +1,5 @@
import json
from http import HTTPMethod

import pytest
import requests
Expand Down Expand Up @@ -101,3 +102,47 @@ def invoke_api(url):
# retry is necessary against AWS, probably IAM permission delay
invoke_response = retry(invoke_api, sleep=2, retries=10, url=invocation_url)
snapshot.match("http-invocation-lambda-url", invoke_response)


@markers.aws.validated
@pytest.mark.parametrize("integration_type", ["HTTP", "HTTP_PROXY"])
def test_http_integration_invoke_status_code_passthrough(
aws_client,
create_status_code_echo_server,
create_rest_api_with_integration,
snapshot,
integration_type,
):
# Create echo serve
echo_server_url = create_status_code_echo_server()
# Create apigw
stage_name = "test"
apigw_id = create_rest_api_with_integration(
integration_uri=f"{echo_server_url}{{map}}",
integration_type=integration_type,
path_part="{map+}",
req_parameters={
"integration.request.path.map": "method.request.path.map",
},
stage=stage_name,
)

def invoke_api(url: str, method: HTTPMethod = HTTPMethod.POST):
response = requests.request(url=url, method=method)
status_code = response.status_code
assert status_code != 403
return {"body": response.json(), "status_code": status_code}

invocation_url = api_invoke_url(
api_id=apigw_id,
stage=stage_name,
path="/status",
)

# Invoke with matching response code
invoke_response = retry(invoke_api, sleep=2, retries=10, url=f"{invocation_url}/200")
snapshot.match("matching-response", invoke_response)

# invoke non matching response code
invoke_response = retry(invoke_api, sleep=2, retries=10, url=f"{invocation_url}/400")
snapshot.match("non-matching-response", invoke_response)
38 changes: 38 additions & 0 deletions tests/aws/services/apigateway/test_apigateway_http.snapshot.json
Expand Up @@ -92,5 +92,43 @@
"status_code": 200
}
}
},
"tests/aws/services/apigateway/test_apigateway_http.py::test_http_integration_invoke_status_code_passthrough[HTTP]": {
"recorded-date": "01-04-2024, 21:02:52",
"recorded-content": {
"matching-response": {
"body": {
"message": "",
"status_code": 200
},
"status_code": 200
},
"non-matching-response": {
"body": {
"message": "",
"status_code": 400
},
"status_code": 200
}
}
},
"tests/aws/services/apigateway/test_apigateway_http.py::test_http_integration_invoke_status_code_passthrough[HTTP_PROXY]": {
"recorded-date": "01-04-2024, 20:44:46",
"recorded-content": {
"matching-response": {
"body": {
"message": "",
"status_code": 200
},
"status_code": 200
},
"non-matching-response": {
"body": {
"message": "",
"status_code": 400
},
"status_code": 400
}
}
}
}
@@ -1,9 +1,12 @@
{
"tests/aws/services/apigateway/test_apigateway_http.py::test_http_integration_with_lambda": {
"last_validated_date": "2024-03-28T19:24:09+00:00"
"tests/aws/services/apigateway/test_apigateway_http.py::test_http_integration_invoke_status_code_passthrough[HTTP]": {
"last_validated_date": "2024-04-01T21:45:48+00:00"
},
"tests/aws/services/apigateway/test_apigateway_http.py::test_http_integration_invoke_status_code_passthrough[HTTP_PROXY]": {
"last_validated_date": "2024-04-01T21:46:23+00:00"
},
"tests/aws/services/apigateway/test_apigateway_http.py::test_http_integration_with_lambda[HTTP]": {
"last_validated_date": "2024-03-29T15:12:45+00:00"
"last_validated_date": "2024-04-01T21:51:36+00:00"
},
"tests/aws/services/apigateway/test_apigateway_http.py::test_http_integration_with_lambda[HTTP_PROXY]": {
"last_validated_date": "2024-03-29T15:13:24+00:00"
Expand Down
19 changes: 19 additions & 0 deletions tests/aws/services/lambda_/functions/lambda_echo_status_code.py
@@ -0,0 +1,19 @@
import json
from http import HTTPStatus


def make_response(status_code: int, message: str):
return {
"statusCode": status_code,
"headers": {"Content-Type": "application/json"},
"body": {"status_code": status_code, "message": message},
}


def handler(event, context):
print(json.dumps(event))
path: str = event["requestContext"]["http"].get("path", "")
status_code = path.split("/")[-1]
if not status_code.isdigit() or int(status_code) not in list(HTTPStatus):
return make_response(HTTPStatus.BAD_REQUEST, f"No valid status found at end of path {path}")
return make_response(int(status_code), "")
3 changes: 3 additions & 0 deletions tests/aws/services/lambda_/test_lambda.py
Expand Up @@ -49,6 +49,9 @@
TEST_LAMBDA_PYTHON = os.path.join(THIS_FOLDER, "functions/lambda_integration.py")
TEST_LAMBDA_PYTHON_ECHO = os.path.join(THIS_FOLDER, "functions/lambda_echo.py")
TEST_LAMBDA_PYTHON_ECHO_JSON_BODY = os.path.join(THIS_FOLDER, "functions/lambda_echo_json_body.py")
TEST_LAMBDA_PYTHON_ECHO_STATUS_CODE = os.path.join(
THIS_FOLDER, "functions/lambda_echo_status_code.py"
)
TEST_LAMBDA_PYTHON_REQUEST_ID = os.path.join(THIS_FOLDER, "functions/lambda_request_id.py")
TEST_LAMBDA_PYTHON_ECHO_VERSION_ENV = os.path.join(
THIS_FOLDER, "functions/lambda_echo_version_env.py"
Expand Down

0 comments on commit 85486d2

Please sign in to comment.