Skip to content

Commit 51884ac

Browse files
authored
Merge pull request #2 from pyk/run-httpapi-locally
Run httpapi locally
2 parents 93d54f8 + fa34f30 commit 51884ac

8 files changed

Lines changed: 845 additions & 87 deletions

File tree

samcli/lib/providers/sam_api_provider.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ class SamApiProvider(CfnBaseApiProvider):
1717
SERVERLESS_API = "AWS::Serverless::Api"
1818
SERVERLESS_HTTP_API = "AWS::Serverless::HttpApi"
1919
TYPES = [SERVERLESS_FUNCTION, SERVERLESS_API, SERVERLESS_HTTP_API]
20-
_FUNCTION_EVENT_TYPE_APIS = ["Api", "HttpApi"]
20+
_EVENT_TYPE_API = "Api"
21+
_EVENT_TYPE_HTTP_API = "HttpApi"
2122
_FUNCTION_EVENT = "Events"
2223
_EVENT_PATH = "Path"
2324
_EVENT_METHOD = "Method"
@@ -270,8 +271,8 @@ def extract_routes_from_events(self, function_logical_id, serverless_function_ev
270271
"""
271272
count = 0
272273
for _, event in serverless_function_events.items():
273-
274-
if event.get(self._EVENT_TYPE) in self._FUNCTION_EVENT_TYPE_APIS:
274+
event_type = event.get(self._EVENT_TYPE)
275+
if event_type in [self._EVENT_TYPE_API, self._EVENT_TYPE_HTTP_API]:
275276
route_resource_id, route = self._convert_event_route(function_logical_id, event.get("Properties"))
276277
collector.add_routes(route_resource_id, [route])
277278
count += 1
@@ -289,6 +290,7 @@ def _convert_event_route(lambda_logical_id, event_properties):
289290
"""
290291
path = event_properties.get(SamApiProvider._EVENT_PATH)
291292
method = event_properties.get(SamApiProvider._EVENT_METHOD)
293+
event_type = event_properties.get(SamApiProvider._EVENT_TYPE)
292294

293295
# An API Event, can have RestApiId property which designates the resource that owns this API. If omitted,
294296
# the API is owned by Implicit API resource. This could either be a direct resource logical ID or a
@@ -305,7 +307,10 @@ def _convert_event_route(lambda_logical_id, event_properties):
305307
"It should either be a LogicalId string or a Ref of a Logical Id string".format(lambda_logical_id)
306308
)
307309

308-
return api_resource_id, Route(path=path, methods=[method], function_name=lambda_logical_id)
310+
return (
311+
api_resource_id,
312+
Route(path=path, methods=[method], function_name=lambda_logical_id, event_type=event_type),
313+
)
309314

310315
@staticmethod
311316
def merge_routes(collector):

samcli/local/apigw/local_apigw_service.py

Lines changed: 131 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@
1111
from samcli.local.services.base_local_service import BaseLocalService, LambdaOutputParser
1212
from samcli.lib.utils.stream_writer import StreamWriter
1313
from samcli.local.lambdafn.exceptions import FunctionNotFound
14-
from samcli.local.events.api_event import ContextIdentity, RequestContext, ApiGatewayLambdaEvent
14+
from samcli.local.events.api_event import (
15+
ContextIdentity,
16+
ContextHTTP,
17+
RequestContext,
18+
RequestContextV2,
19+
ApiGatewayLambdaEvent,
20+
ApiGatewayV2LambdaEvent,
21+
)
1522
from .service_error_responses import ServiceErrorResponses
1623
from .path_converter import PathConverter
1724

@@ -25,9 +32,11 @@ class LambdaResponseParseException(Exception):
2532

2633

2734
class Route:
35+
API = "Api"
36+
HTTP = "HttpApi"
2837
ANY_HTTP_METHODS = ["GET", "DELETE", "PUT", "POST", "HEAD", "OPTIONS", "PATCH"]
2938

30-
def __init__(self, function_name, path, methods):
39+
def __init__(self, function_name, path, methods, event_type=API):
3140
"""
3241
Creates an ApiGatewayRoute
3342
@@ -38,6 +47,7 @@ def __init__(self, function_name, path, methods):
3847
self.methods = self.normalize_method(methods)
3948
self.function_name = function_name
4049
self.path = path
50+
self.event_type = event_type
4151

4252
def __eq__(self, other):
4353
return (
@@ -181,15 +191,26 @@ def _request_handler(self, **kwargs):
181191
route = self._get_current_route(request)
182192
cors_headers = Cors.cors_to_headers(self.api.cors)
183193

184-
method, _ = self.get_request_methods_endpoints(request)
194+
method, endpoint = self.get_request_methods_endpoints(request)
195+
route_key = self._route_key(method, endpoint)
185196
if method == "OPTIONS" and self.api.cors:
186197
headers = Headers(cors_headers)
187198
return self.service_response("", headers, 200)
188199

189200
try:
190-
event = self._construct_event(
191-
request, self.port, self.api.binary_media_types, self.api.stage_name, self.api.stage_variables
192-
)
201+
if route.event_type == Route.HTTP:
202+
event = self._construct_event_http(
203+
request,
204+
self.port,
205+
self.api.binary_media_types,
206+
self.api.stage_name,
207+
self.api.stage_variables,
208+
route_key,
209+
)
210+
else:
211+
event = self._construct_event(
212+
request, self.port, self.api.binary_media_types, self.api.stage_name, self.api.stage_variables
213+
)
193214
except UnicodeDecodeError:
194215
return ServiceErrorResponses.lambda_failure_response()
195216

@@ -411,14 +432,14 @@ def _construct_event(flask_request, port, binary_types, stage_name=None, stage_v
411432
# Flask does not parse/decode the request data. We should do it ourselves
412433
request_data = request_data.decode("utf-8")
413434

435+
query_string_dict, multi_value_query_string_dict = LocalApigwService._query_string_params(flask_request)
436+
414437
context = RequestContext(
415438
resource_path=endpoint, http_method=method, stage=stage_name, identity=identity, path=endpoint
416439
)
417440

418441
headers_dict, multi_value_headers_dict = LocalApigwService._event_headers(flask_request, port)
419442

420-
query_string_dict, multi_value_query_string_dict = LocalApigwService._query_string_params(flask_request)
421-
422443
event = ApiGatewayLambdaEvent(
423444
http_method=method,
424445
body=request_data,
@@ -438,6 +459,59 @@ def _construct_event(flask_request, port, binary_types, stage_name=None, stage_v
438459
LOG.debug("Constructed String representation of Event to invoke Lambda. Event: %s", event_str)
439460
return event_str
440461

462+
@staticmethod
463+
def _construct_event_http(flask_request, port, binary_types, stage_name=None, stage_variables=None, route_key=None):
464+
"""
465+
Helper method that constructs the Event 2.0 to be passed to Lambda
466+
467+
https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html
468+
469+
:param request flask_request: Flask Request
470+
:return: String representing the event
471+
"""
472+
# pylint: disable-msg=too-many-locals
473+
474+
endpoint = PathConverter.convert_path_to_api_gateway(flask_request.endpoint)
475+
method = flask_request.method
476+
477+
request_data = flask_request.get_data()
478+
479+
request_mimetype = flask_request.mimetype
480+
481+
is_base_64 = LocalApigwService._should_base64_encode(binary_types, request_mimetype)
482+
483+
if is_base_64:
484+
LOG.debug("Incoming Request seems to be binary. Base64 encoding the request data before sending to Lambda.")
485+
request_data = base64.b64encode(request_data)
486+
487+
if request_data:
488+
# Flask does not parse/decode the request data. We should do it ourselves
489+
request_data = request_data.decode("utf-8")
490+
491+
query_string_dict, _ = LocalApigwService._query_string_params(flask_request)
492+
493+
cookies = LocalApigwService._event_http_cookies(flask_request)
494+
headers = LocalApigwService._event_http_headers(flask_request, port)
495+
context_http = ContextHTTP(method=method, path=flask_request.path, source_ip=flask_request.remote_addr)
496+
context = RequestContextV2(http=context_http, route_key=route_key, stage=stage_name)
497+
event = ApiGatewayV2LambdaEvent(
498+
route_key=route_key,
499+
raw_path=flask_request.path,
500+
raw_query_string=flask_request.query_string.decode("utf-8"),
501+
cookies=cookies,
502+
headers=headers,
503+
query_string_params=query_string_dict,
504+
request_context=context,
505+
body=request_data,
506+
path_parameters=flask_request.view_args,
507+
is_base_64_encoded=is_base_64,
508+
stage_variables=stage_variables,
509+
)
510+
511+
event_str = json.dumps(event.to_dict())
512+
LOG.debug("Constructed String representation of Event Version 2.0 to invoke Lambda. Event: %s", event_str)
513+
return event_str
514+
441515
@staticmethod
442516
def _query_string_params(flask_request):
443517
"""
@@ -506,6 +580,55 @@ def _event_headers(flask_request, port):
506580
multi_value_headers_dict["X-Forwarded-Port"] = [str(port)]
507581
return headers_dict, multi_value_headers_dict
508582

583+
@staticmethod
584+
def _event_http_cookies(flask_request):
585+
"""
586+
All cookie headers in the request are combined with commas.
587+
588+
https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html
589+
590+
Parameters
591+
----------
592+
flask_request request
593+
Request from Flask
594+
595+
Returns list
596+
-------
597+
Returns a list of cookies
598+
599+
"""
600+
cookies = []
601+
for cookie_key in flask_request.cookies.keys():
602+
cookies.append("{}={}".format(cookie_key, flask_request.cookies.get(cookie_key)))
603+
return cookies
604+
605+
@staticmethod
606+
def _event_http_headers(flask_request, port):
607+
"""
608+
Duplicate headers are combined with commas.
609+
610+
https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html
611+
612+
Parameters
613+
----------
614+
flask_request request
615+
Request from Flask
616+
617+
Returns list
618+
-------
619+
Returns a list of cookies
620+
621+
"""
622+
headers = {}
623+
# Multi-value request headers is not really supported by Flask.
624+
# See https://github.com/pallets/flask/issues/850
625+
for header_key in flask_request.headers.keys():
626+
headers[header_key] = flask_request.headers.get(header_key)
627+
628+
headers["X-Forwarded-Proto"] = flask_request.scheme
629+
headers["X-Forwarded-Port"] = str(port)
630+
return headers
631+
509632
@staticmethod
510633
def _should_base64_encode(binary_types, request_mimetype):
511634
"""

0 commit comments

Comments
 (0)