Skip to content

Commit ed9cd64

Browse files
committed
fix TestInvokeMethod 500 failures
1 parent 58dcd87 commit ed9cd64

File tree

9 files changed

+209
-27
lines changed

9 files changed

+209
-27
lines changed

localstack-core/localstack/services/apigateway/next_gen/execute_api/integrations/http.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from localstack.aws.api.apigateway import Integration
99

1010
from ..context import EndpointResponse, IntegrationRequest, RestApiInvocationContext
11-
from ..gateway_response import ApiConfigurationError, IntegrationFailureError
11+
from ..gateway_response import ApiConfigurationError, IntegrationFailureError, InternalServerError
1212
from ..header_utils import build_multi_value_headers
1313
from .core import RestApiIntegration
1414

@@ -72,7 +72,7 @@ def invoke(self, context: RestApiInvocationContext) -> EndpointResponse:
7272
except (requests.exceptions.InvalidURL, requests.exceptions.InvalidSchema) as e:
7373
LOG.warning("Execution failed due to configuration error: Invalid endpoint address")
7474
LOG.debug("The URI specified for the HTTP/HTTP_PROXY integration is invalid: %s", uri)
75-
raise ApiConfigurationError("Internal server error") from e
75+
raise InternalServerError("Internal server error") from e
7676

7777
except (requests.exceptions.Timeout, requests.exceptions.SSLError) as e:
7878
# TODO make the exception catching more fine grained
@@ -127,7 +127,7 @@ def invoke(self, context: RestApiInvocationContext) -> EndpointResponse:
127127
except (requests.exceptions.InvalidURL, requests.exceptions.InvalidSchema) as e:
128128
LOG.warning("Execution failed due to configuration error: Invalid endpoint address")
129129
LOG.debug("The URI specified for the HTTP/HTTP_PROXY integration is invalid: %s", uri)
130-
raise ApiConfigurationError("Internal server error") from e
130+
raise InternalServerError("Internal server error") from e
131131

132132
except (requests.exceptions.Timeout, requests.exceptions.SSLError):
133133
# TODO make the exception catching more fine grained

localstack-core/localstack/services/apigateway/next_gen/execute_api/test_invoke.py

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,17 @@
6262
{formatted_date} : Method completed with status: {method_response_status}
6363
"""
6464

65+
TEST_INVOKE_TEMPLATE_FAILED = """Execution log for request {request_id}
66+
{formatted_date} : Starting execution for request: {request_id}
67+
{formatted_date} : HTTP Method: {request_method}, Resource Path: {resource_path}
68+
{formatted_date} : Method request path: {method_request_path_parameters}
69+
{formatted_date} : Method request query string: {method_request_query_string}
70+
{formatted_date} : Method request headers: {method_request_headers}
71+
{formatted_date} : Method request body before transformations: {method_request_body}
72+
{formatted_date} : Execution failed due to {error_type}: {error_message}
73+
{formatted_date} : Method completed with status: {method_response_status}
74+
"""
75+
6576

6677
def _dump_headers(headers: Headers) -> str:
6778
if not headers:
@@ -80,9 +91,9 @@ def log_template(invocation_context: RestApiInvocationContext, response_headers:
8091
formatted_date = datetime.datetime.now(tz=datetime.UTC).strftime("%a %b %d %H:%M:%S %Z %Y")
8192
request = invocation_context.invocation_request
8293
context_var = invocation_context.context_variables
83-
integration_req = invocation_context.integration_request
84-
endpoint_resp = invocation_context.endpoint_response
85-
method_resp = invocation_context.invocation_response
94+
integration_req = invocation_context.integration_request or {}
95+
endpoint_resp = invocation_context.endpoint_response or {}
96+
method_resp = invocation_context.invocation_response or {}
8697
# TODO: if endpoint_uri is an ARN, it means it's an AWS_PROXY integration
8798
# this should be transformed to the true URL of a lambda invoke call
8899
endpoint_uri = integration_req.get("uri", "")
@@ -116,7 +127,7 @@ def log_mock_template(
116127
formatted_date = datetime.datetime.now(tz=datetime.UTC).strftime("%a %b %d %H:%M:%S %Z %Y")
117128
request = invocation_context.invocation_request
118129
context_var = invocation_context.context_variables
119-
method_resp = invocation_context.invocation_response
130+
method_resp = invocation_context.invocation_response or {}
120131

121132
return TEST_INVOKE_TEMPLATE_MOCK.format(
122133
formatted_date=formatted_date,
@@ -133,6 +144,29 @@ def log_mock_template(
133144
)
134145

135146

147+
def log_failed_template(
148+
invocation_context: RestApiInvocationContext, response_status_code: int
149+
) -> str:
150+
formatted_date = datetime.datetime.now(tz=datetime.UTC).strftime("%a %b %d %H:%M:%S %Z %Y")
151+
request = invocation_context.invocation_request
152+
context_var = invocation_context.context_variables
153+
154+
return TEST_INVOKE_TEMPLATE_FAILED.format(
155+
formatted_date=formatted_date,
156+
request_id=context_var["requestId"],
157+
resource_path=request["path"],
158+
request_method=request["http_method"],
159+
method_request_path_parameters=dict_to_string(request["path_parameters"]),
160+
method_request_query_string=dict_to_string(request["query_string_parameters"]),
161+
method_request_headers=_dump_headers(request.get("headers")),
162+
method_request_body=to_str(request.get("body", "")),
163+
method_response_status=response_status_code,
164+
# TODO: fix the error message
165+
error_type="",
166+
error_message="",
167+
)
168+
169+
136170
def create_test_chain() -> HandlerChain[RestApiInvocationContext]:
137171
return HandlerChain(
138172
request_handlers=[
@@ -216,7 +250,9 @@ def create_test_invocation_context(
216250
responseOverride=ContextVarsResponseOverride(header={}, status=0),
217251
)
218252
invocation_context.trace_id = parse_handler.populate_trace_id({})
219-
resource_method = resource["resourceMethods"][http_method]
253+
resource_method = (
254+
resource["resourceMethods"].get(http_method) or resource["resourceMethods"]["ANY"]
255+
)
220256
invocation_context.resource = resource
221257
invocation_context.resource_method = resource_method
222258
invocation_context.integration = resource_method["methodIntegration"]
@@ -256,7 +292,15 @@ def run_test_invocation(
256292
# AWS does not return the Content-Length for TestInvokeMethod
257293
response_headers.remove("Content-Length")
258294

259-
if is_mock_integration:
295+
if not invocation_context.invocation_response:
296+
# TODO: this is an heuristic to guess if we encounter an exception in the call
297+
# in the future, we should attach the exception to the context so we could act on it and properly
298+
# log as we go through the invocation, so that if we have an error we stop logging at the right moment
299+
for header in ("Content-Type", "X-Amzn-Trace-Id"):
300+
response_headers.remove(header)
301+
log = log_failed_template(invocation_context, test_response.status_code)
302+
303+
elif is_mock_integration:
260304
# TODO: revisit how we're building the logs
261305
log = log_mock_template(invocation_context, response_headers)
262306
else:

localstack-core/localstack/services/apigateway/next_gen/provider.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,11 @@ def test_invoke_method(
429429
if not resource:
430430
raise NotFoundException("Invalid Resource identifier specified")
431431

432+
resource_methods = resource.resource_methods
433+
434+
if request["httpMethod"] not in resource_methods and "ANY" not in resource_methods:
435+
raise NotFoundException("Invalid Method identifier specified")
436+
432437
# test httpMethod
433438

434439
rest_api_container = get_rest_api_container(context, rest_api_id=rest_api_id)

tests/aws/services/apigateway/test_apigateway_api.py

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2884,7 +2884,18 @@ def invoke_method(
28842884
apigw_test_invoke_response_formatter(response),
28852885
)
28862886

2887-
# assert resource and rest api doesn't exist
2887+
# assert method doesn't exist
2888+
with pytest.raises(ClientError) as ex:
2889+
aws_client.apigateway.test_invoke_method(
2890+
restApiId=rest_api_id,
2891+
resourceId=resource_id,
2892+
httpMethod="HEAD",
2893+
pathWithQueryString="/pets/123",
2894+
body=json.dumps({"foo": "bar"}),
2895+
)
2896+
snapshot.match("resource-method-not-found", ex.value.response)
2897+
2898+
# assert resource doesn't exist
28882899
with pytest.raises(ClientError) as ex:
28892900
aws_client.apigateway.test_invoke_method(
28902901
restApiId=rest_api_id,
@@ -2895,6 +2906,7 @@ def invoke_method(
28952906
)
28962907
snapshot.match("resource-id-not-found", ex.value.response)
28972908

2909+
# assert API doesn't exist
28982910
with pytest.raises(ClientError) as ex:
28992911
aws_client.apigateway.test_invoke_method(
29002912
restApiId=rest_api_id,
@@ -2905,6 +2917,69 @@ def invoke_method(
29052917
)
29062918
snapshot.match("rest-api-not-found", ex.value.response)
29072919

2920+
@markers.aws.validated
2921+
@markers.snapshot.skip_snapshot_verify(
2922+
# TODO: our way of handling logs for TestInvokeMethod is too naive to properly handle all
2923+
# type of exceptions (we'll need to build logs as we progress through the invocation)
2924+
paths=[
2925+
"$..log.line07",
2926+
]
2927+
)
2928+
def test_failed_invoke_test_method(
2929+
self, create_rest_apigw, snapshot, aws_client, apigw_test_invoke_response_formatter
2930+
):
2931+
rest_api_id, _, root_resource_id = create_rest_apigw(name="test failed invoke")
2932+
2933+
aws_client.apigateway.put_method(
2934+
restApiId=rest_api_id,
2935+
resourceId=root_resource_id,
2936+
httpMethod="ANY",
2937+
authorizationType="NONE",
2938+
)
2939+
2940+
aws_client.apigateway.put_integration(
2941+
restApiId=rest_api_id,
2942+
resourceId=root_resource_id,
2943+
httpMethod="ANY",
2944+
type="HTTP_PROXY",
2945+
uri="https://${stageVariables.testHost}",
2946+
integrationHttpMethod="ANY",
2947+
)
2948+
# we are going to not declare this stage variable on purpose to make the call fail
2949+
2950+
def invoke_method(
2951+
api_id: str,
2952+
target_resource_id: str,
2953+
method: str,
2954+
path_with_query_string: str | None = None,
2955+
body: str = "",
2956+
):
2957+
kwargs = {}
2958+
if path_with_query_string is not None:
2959+
kwargs["pathWithQueryString"] = path_with_query_string
2960+
res = aws_client.apigateway.test_invoke_method(
2961+
restApiId=api_id,
2962+
resourceId=target_resource_id,
2963+
httpMethod=method,
2964+
body=body,
2965+
**kwargs,
2966+
)
2967+
assert res.get("status") == 500
2968+
return res
2969+
2970+
response = retry(
2971+
invoke_method,
2972+
retries=10,
2973+
sleep=5,
2974+
api_id=rest_api_id,
2975+
target_resource_id=root_resource_id,
2976+
method="GET",
2977+
)
2978+
snapshot.match(
2979+
"test-invoke-failure",
2980+
apigw_test_invoke_response_formatter(response),
2981+
)
2982+
29082983

29092984
class TestApigatewayIntegration:
29102985
@markers.aws.validated

tests/aws/services/apigateway/test_apigateway_api.snapshot.json

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2813,7 +2813,7 @@
28132813
}
28142814
},
28152815
"tests/aws/services/apigateway/test_apigateway_api.py::TestApigatewayTestInvoke::test_invoke_test_method": {
2816-
"recorded-date": "19-08-2025, 17:20:41",
2816+
"recorded-date": "29-09-2025, 19:28:50",
28172817
"recorded-content": {
28182818
"test-invoke-method-get-pets": {
28192819
"body": {
@@ -3046,6 +3046,17 @@
30463046
"HTTPStatusCode": 200
30473047
}
30483048
},
3049+
"resource-method-not-found": {
3050+
"Error": {
3051+
"Code": "NotFoundException",
3052+
"Message": "Invalid Method identifier specified"
3053+
},
3054+
"message": "Invalid Method identifier specified",
3055+
"ResponseMetadata": {
3056+
"HTTPHeaders": {},
3057+
"HTTPStatusCode": 404
3058+
}
3059+
},
30493060
"resource-id-not-found": {
30503061
"Error": {
30513062
"Code": "NotFoundException",
@@ -4861,5 +4872,41 @@
48614872
}
48624873
}
48634874
}
4875+
},
4876+
"tests/aws/services/apigateway/test_apigateway_api.py::TestApigatewayTestInvoke::test_failed_invoke_test_method": {
4877+
"recorded-date": "29-09-2025, 19:44:14",
4878+
"recorded-content": {
4879+
"test-invoke-failure": {
4880+
"body": {
4881+
"message": "Internal server error"
4882+
},
4883+
"headers": {
4884+
"x-amzn-ErrorType": "InternalServerErrorException"
4885+
},
4886+
"latency": "<latency>",
4887+
"log": {
4888+
"line00": "Execution log for request <request-id-1>",
4889+
"line01": "DDD MMM dd hh:mm:ss UTC yyyy : Starting execution for request: <request-id-1>",
4890+
"line02": "DDD MMM dd hh:mm:ss UTC yyyy : HTTP Method: GET, Resource Path: /",
4891+
"line03": "DDD MMM dd hh:mm:ss UTC yyyy : Method request path: {}",
4892+
"line04": "DDD MMM dd hh:mm:ss UTC yyyy : Method request query string: {}",
4893+
"line05": "DDD MMM dd hh:mm:ss UTC yyyy : Method request headers: {}",
4894+
"line06": "DDD MMM dd hh:mm:ss UTC yyyy : Method request body before transformations: ",
4895+
"line07": "DDD MMM dd hh:mm:ss UTC yyyy : Execution failed due to configuration error: Invalid endpoint address",
4896+
"line08": "DDD MMM dd hh:mm:ss UTC yyyy : Method completed with status: 500",
4897+
"line09": ""
4898+
},
4899+
"multiValueHeaders": {
4900+
"x-amzn-ErrorType": [
4901+
"InternalServerErrorException"
4902+
]
4903+
},
4904+
"status": 500,
4905+
"ResponseMetadata": {
4906+
"HTTPHeaders": {},
4907+
"HTTPStatusCode": 200
4908+
}
4909+
}
4910+
}
48644911
}
48654912
}

tests/aws/services/apigateway/test_apigateway_api.validation.json

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -389,13 +389,22 @@
389389
"total": 3.98
390390
}
391391
},
392+
"tests/aws/services/apigateway/test_apigateway_api.py::TestApigatewayTestInvoke::test_failed_invoke_test_method": {
393+
"last_validated_date": "2025-09-29T19:44:14+00:00",
394+
"durations_in_seconds": {
395+
"setup": 0.81,
396+
"call": 1.14,
397+
"teardown": 0.67,
398+
"total": 2.62
399+
}
400+
},
392401
"tests/aws/services/apigateway/test_apigateway_api.py::TestApigatewayTestInvoke::test_invoke_test_method": {
393-
"last_validated_date": "2025-08-19T17:20:41+00:00",
402+
"last_validated_date": "2025-09-29T19:28:50+00:00",
394403
"durations_in_seconds": {
395-
"setup": 0.83,
396-
"call": 4.12,
404+
"setup": 0.86,
405+
"call": 4.31,
397406
"teardown": 0.67,
398-
"total": 5.62
407+
"total": 5.84
399408
}
400409
}
401410
}

tests/aws/services/apigateway/test_apigateway_import.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -802,15 +802,14 @@ def call_api():
802802

803803
url_error = api_invoke_url(api_id=rest_api_id, stage="v2", path="/path1")
804804

805-
def call_api_error():
805+
def call_api_error() -> requests.Response:
806806
res = requests.get(url_error)
807807
assert res.status_code == 500
808-
return res.json()
808+
return res
809809

810810
resp = retry(call_api_error, retries=5, sleep=2)
811-
# we remove the headers from the response, not really needed for this test
812-
resp.pop("headers", None)
813-
snapshot.match("get-error-resp-from-http", resp)
811+
error = {"body": resp.json(), "errorType": resp.headers.get("x-amzn-ErrorType")}
812+
snapshot.match("get-error-resp-from-http", error)
814813

815814
@markers.aws.validated
816815
@markers.snapshot.skip_snapshot_verify(

tests/aws/services/apigateway/test_apigateway_import.snapshot.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4823,7 +4823,7 @@
48234823
}
48244824
},
48254825
"tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_with_stage_variables": {
4826-
"recorded-date": "02-07-2025, 16:31:50",
4826+
"recorded-date": "29-09-2025, 20:07:21",
48274827
"recorded-content": {
48284828
"get-resp-from-http": {
48294829
"args": {
@@ -4836,7 +4836,10 @@
48364836
"path": "/test-path"
48374837
},
48384838
"get-error-resp-from-http": {
4839-
"message": "Internal server error"
4839+
"body": {
4840+
"message": "Internal server error"
4841+
},
4842+
"errorType": "InternalServerErrorException"
48404843
}
48414844
}
48424845
},

tests/aws/services/apigateway/test_apigateway_import.validation.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,12 +144,12 @@
144144
}
145145
},
146146
"tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_with_stage_variables": {
147-
"last_validated_date": "2025-07-02T16:31:50+00:00",
147+
"last_validated_date": "2025-09-29T20:07:21+00:00",
148148
"durations_in_seconds": {
149-
"setup": 0.11,
150-
"call": 5.83,
151-
"teardown": 69.73,
152-
"total": 75.67
149+
"setup": 11.64,
150+
"call": 11.14,
151+
"teardown": 2.26,
152+
"total": 25.04
153153
}
154154
},
155155
"tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_put_rest_api_mode_binary_media_types[merge]": {

0 commit comments

Comments
 (0)