Skip to content

Commit d343815

Browse files
authored
APIGW NG implement AWS bug for INTEGRATION_FAILURE GatewayResponse (#11183)
1 parent 30249e8 commit d343815

File tree

2 files changed

+65
-35
lines changed

2 files changed

+65
-35
lines changed

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

Lines changed: 35 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,38 @@
88
)
99

1010

11+
class GatewayResponseCode(StatusCode, Enum):
12+
REQUEST_TOO_LARGE = "413"
13+
RESOURCE_NOT_FOUND = "404"
14+
AUTHORIZER_CONFIGURATION_ERROR = "500"
15+
MISSING_AUTHENTICATION_TOKEN = "403"
16+
BAD_REQUEST_BODY = "400"
17+
INVALID_SIGNATURE = "403"
18+
INVALID_API_KEY = "403"
19+
BAD_REQUEST_PARAMETERS = "400"
20+
AUTHORIZER_FAILURE = "500"
21+
UNAUTHORIZED = "401"
22+
INTEGRATION_TIMEOUT = "504"
23+
ACCESS_DENIED = "403"
24+
DEFAULT_4XX = ""
25+
DEFAULT_5XX = ""
26+
WAF_FILTERED = "403"
27+
QUOTA_EXCEEDED = "429"
28+
THROTTLED = "429"
29+
API_CONFIGURATION_ERROR = "500"
30+
UNSUPPORTED_MEDIA_TYPE = "415"
31+
INTEGRATION_FAILURE = "504"
32+
EXPIRED_TOKEN = "403"
33+
34+
1135
class BaseGatewayException(Exception):
1236
"""
1337
Base class for all Gateway exceptions
1438
Do not raise from this class directly. Instead, raise the specific Exception
1539
"""
1640

1741
message: str = "Unimplemented Response"
18-
type: GatewayResponseType
42+
type: GatewayResponseType = None
1943
status_code: int | str = None
2044
code: str = ""
2145

@@ -24,14 +48,17 @@ def __init__(self, message: str = None, status_code: int | str = None):
2448
self.message = message
2549
if status_code is not None:
2650
self.status_code = status_code
51+
elif self.status_code is None and self.type:
52+
# Fallback to the default value
53+
self.status_code = GatewayResponseCode[self.type]
2754

2855

2956
class Default4xxError(BaseGatewayException):
3057
"""Do not raise from this class directly.
3158
Use one of the subclasses instead, as they contain the appropriate header
3259
"""
3360

34-
type: GatewayResponseType.DEFAULT_4XX
61+
type = GatewayResponseType.DEFAULT_4XX
3562
status_code = 400
3663

3764

@@ -40,7 +67,7 @@ class Default5xxError(BaseGatewayException):
4067
Use one of the subclasses instead, as they contain the appropriate header
4168
"""
4269

43-
type: GatewayResponseType.DEFAULT_5XX
70+
type = GatewayResponseType.DEFAULT_5XX
4471
status_code = 500
4572

4673

@@ -98,8 +125,9 @@ class ExpiredTokenError(BaseGatewayException):
98125

99126
class IntegrationFailureError(BaseGatewayException):
100127
type = GatewayResponseType.INTEGRATION_FAILURE
101-
# TODO validate this header with aws validated tests
102-
code = "IntegrationFailureException"
128+
# TODO: tested manually for now
129+
code = "InternalServerErrorException"
130+
status_code = 500
103131

104132

105133
class IntegrationTimeoutError(BaseGatewayException):
@@ -161,30 +189,6 @@ class WafFilteredError(BaseGatewayException):
161189
code = "WafFilteredException"
162190

163191

164-
class GatewayResponseCode(StatusCode, Enum):
165-
REQUEST_TOO_LARGE = "413"
166-
RESOURCE_NOT_FOUND = "404"
167-
AUTHORIZER_CONFIGURATION_ERROR = "500"
168-
MISSING_AUTHENTICATION_TOKEN = "403"
169-
BAD_REQUEST_BODY = "400"
170-
INVALID_SIGNATURE = "403"
171-
INVALID_API_KEY = "403"
172-
BAD_REQUEST_PARAMETERS = "400"
173-
AUTHORIZER_FAILURE = "500"
174-
UNAUTHORIZED = "401"
175-
INTEGRATION_TIMEOUT = "504"
176-
ACCESS_DENIED = "403"
177-
DEFAULT_4XX = ""
178-
DEFAULT_5XX = ""
179-
WAF_FILTERED = "403"
180-
QUOTA_EXCEEDED = "429"
181-
THROTTLED = "429"
182-
API_CONFIGURATION_ERROR = "500"
183-
UNSUPPORTED_MEDIA_TYPE = "415"
184-
INTEGRATION_FAILURE = "504"
185-
EXPIRED_TOKEN = "403"
186-
187-
188192
def build_gateway_response(
189193
response_type: GatewayResponseType,
190194
status_code: StatusCode = None,
@@ -199,10 +203,9 @@ def build_gateway_response(
199203
or {"application/json": '{"message":$context.error.messageString}'},
200204
responseType=response_type,
201205
defaultResponse=default_response,
206+
statusCode=status_code,
202207
)
203-
if status_code or (status_code := GatewayResponseCode[response_type]):
204-
# DEFAULT_4XX and DEFAULT_5XX do not have `statusCode` present in the response
205-
response["statusCode"] = status_code
208+
206209
return response
207210

208211

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

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from ..models import apigateway_stores
3131
from .execute_api.gateway_response import (
3232
DEFAULT_GATEWAY_RESPONSES,
33+
GatewayResponseCode,
3334
build_gateway_response,
3435
get_gateway_response_or_default,
3536
)
@@ -163,8 +164,18 @@ def put_gateway_response(
163164
response_type=response_type,
164165
default_response=False,
165166
)
167+
166168
rest_api_container.gateway_responses[response_type] = gateway_response
167-
return gateway_response
169+
170+
# The CRUD provider has a weird behavior: for some responses (for now, INTEGRATION_FAILURE), it sets the default
171+
# status code to `504`. However, in the actual invocation logic, it returns 500. To deal with the inconsistency,
172+
# we need to set the value to None if not provided by the user, so that the invocation logic can properly return
173+
# 500, and the CRUD layer can still return 504 even though it is technically wrong.
174+
response = gateway_response.copy()
175+
if response.get("statusCode") is None:
176+
response["statusCode"] = GatewayResponseCode[response_type]
177+
178+
return response
168179

169180
def get_gateway_response(
170181
self,
@@ -185,7 +196,7 @@ def get_gateway_response(
185196
message=f"1 validation error detected: Value '{response_type}' at 'responseType' failed to satisfy constraint: Member must satisfy enum value set: [{', '.join(DEFAULT_GATEWAY_RESPONSES)}]",
186197
)
187198

188-
gateway_response = get_gateway_response_or_default(
199+
gateway_response = _get_gateway_response_or_default(
189200
response_type, rest_api_container.gateway_responses
190201
)
191202
# TODO: add validation with the parameters? seems like it validated client side? how to try?
@@ -207,7 +218,23 @@ def get_gateway_responses(
207218

208219
user_gateway_resp = rest_api_container.gateway_responses
209220
gateway_responses = [
210-
get_gateway_response_or_default(response_type, user_gateway_resp)
221+
_get_gateway_response_or_default(response_type, user_gateway_resp)
211222
for response_type in DEFAULT_GATEWAY_RESPONSES
212223
]
213224
return GatewayResponses(items=gateway_responses)
225+
226+
227+
def _get_gateway_response_or_default(
228+
response_type: GatewayResponseType,
229+
gateway_responses: dict[GatewayResponseType, GatewayResponse],
230+
) -> GatewayResponse:
231+
"""
232+
Utility function that overrides the behavior of `get_gateway_response_or_default` by setting a default status code
233+
from the `GatewayResponseCode` values. In reality, some default values in the invocation layer are different from
234+
what the CRUD layer of API Gateway is returning.
235+
"""
236+
response = get_gateway_response_or_default(response_type, gateway_responses)
237+
if response.get("statusCode") is None and (status_code := GatewayResponseCode[response_type]):
238+
response["statusCode"] = status_code
239+
240+
return response

0 commit comments

Comments
 (0)