Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(LocalLambdaService): Error handling for local lambda #532

Merged
merged 9 commits into from
Jul 9, 2018
23 changes: 3 additions & 20 deletions samcli/local/apigw/local_apigw_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from flask import Flask, request

from samcli.local.services.base_local_service import BaseLocalService, LambdaOutputParser
from samcli.local.services.base_local_service import BaseLocalService, LambdaOutputParser, CaseInsensitiveDict
from samcli.local.lambdafn.exceptions import FunctionNotFound
from samcli.local.events.api_event import ContextIdentity, RequestContext, ApiGatewayLambdaEvent
from .service_error_responses import ServiceErrorResponses
Expand All @@ -15,23 +15,6 @@
LOG = logging.getLogger(__name__)


class CaseInsensitiveDict(dict):
"""
Implement a simple case insensitive dictionary for storing headers. To preserve the original
case of the given Header (e.g. X-FooBar-Fizz) this only touches the get and contains magic
methods rather than implementing a __setitem__ where we normalize the case of the headers.
"""

def __getitem__(self, key):
matches = [v for k, v in self.items() if k.lower() == key.lower()]
if not matches:
raise KeyError(key)
return matches[0]

def __contains__(self, key):
return key.lower() in [k.lower() for k in self.keys()]


class Route(object):

def __init__(self, methods, function_name, path, binary_types=None):
Expand Down Expand Up @@ -158,7 +141,7 @@ def _request_handler(self, **kwargs):
except FunctionNotFound:
return ServiceErrorResponses.lambda_not_found_response()

lambda_response, lambda_logs = LambdaOutputParser.get_lambda_output(stdout_stream)
lambda_response, lambda_logs, _ = LambdaOutputParser.get_lambda_output(stdout_stream)

if self.stderr and lambda_logs:
# Write the logs to stderr if available.
Expand All @@ -173,7 +156,7 @@ def _request_handler(self, **kwargs):
"statusCode in the response object). Response received: %s", lambda_response)
return ServiceErrorResponses.lambda_failure_response()

return self._service_response(body, headers, status_code)
return self.service_response(body, headers, status_code)

def _get_current_route(self, flask_request):
"""
Expand Down
242 changes: 242 additions & 0 deletions samcli/local/lambda_service/lambda_error_responses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
"""Common Lambda Error Responses"""

import json
from collections import OrderedDict

from samcli.local.services.base_local_service import BaseLocalService


class LambdaErrorResponses(object):

# The content type of the Invoke request body is not JSON.
UnsupportedMediaTypeException = ('UnsupportedMediaType', 415)

# The AWS Lambda service encountered an internal error.
ServiceException = ('Service', 500)

# The resource (for example, a Lambda function or access policy statement) specified in the request does not exist.
ResourceNotFoundException = ('ResourceNotFound', 404)

# The request body could not be parsed as JSON.
InvalidRequestContentException = ('InvalidRequestContent', 400)

NotImplementedException = ('NotImplemented', 501)

PathNotFoundException = ('PathNotFoundLocally', 404)

MethodNotAllowedException = ('MethodNotAllowedLocally', 405)

# Error Types
USER_ERROR = "User"
SERVICE_ERROR = "Service"
LOCAL_SERVICE_ERROR = "LocalService"

# Header Information
CONTENT_TYPE = 'application/json'
CONTENT_TYPE_HEADER_KEY = 'Content-Type'

@staticmethod
def resource_not_found(function_name):
"""
Creates a Lambda Service ResourceNotFound Response

Parameters
----------
function_name str
Name of the function that was requested to invoke

Returns
-------
Flask.Response
A response object representing the ResourceNotFound Error
"""
exception_tuple = LambdaErrorResponses.ResourceNotFoundException

return BaseLocalService.service_response(
LambdaErrorResponses._construct_error_response_body(
LambdaErrorResponses.USER_ERROR,
"Function not found: arn:aws:lambda:us-west-2:012345678901:function:{}".format(function_name)
),
LambdaErrorResponses._construct_headers(exception_tuple[0]),
exception_tuple[1]
)

@staticmethod
def invalid_request_content(message):
"""
Creates a Lambda Service InvalidRequestContent Response

Parameters
----------
message str
Message to be added to the body of the response

Returns
-------
Flask.Response
A response object representing the InvalidRequestContent Error
"""
exception_tuple = LambdaErrorResponses.InvalidRequestContentException

return BaseLocalService.service_response(
LambdaErrorResponses._construct_error_response_body(LambdaErrorResponses.USER_ERROR, message),
LambdaErrorResponses._construct_headers(exception_tuple[0]),
exception_tuple[1]
)

@staticmethod
def unsupported_media_type(content_type):
"""
Creates a Lambda Service UnsupportedMediaType Response

Parameters
----------
content_type str
Content Type of the request that was made

Returns
-------
Flask.Response
A response object representing the UnsupportedMediaType Error
"""
exception_tuple = LambdaErrorResponses.UnsupportedMediaTypeException

return BaseLocalService.service_response(
LambdaErrorResponses._construct_error_response_body(LambdaErrorResponses.USER_ERROR,
"Unsupported content type: {}".format(content_type)),
LambdaErrorResponses._construct_headers(exception_tuple[0]),
exception_tuple[1]
)

@staticmethod
def generic_service_exception(*args):
"""
Creates a Lambda Service Generic ServiceException Response

Parameters
----------
args list
List of arguments Flask passes to the method

Returns
-------
Flask.Response
A response object representing the GenericServiceException Error
"""
exception_tuple = LambdaErrorResponses.ServiceException

return BaseLocalService.service_response(
LambdaErrorResponses._construct_error_response_body(LambdaErrorResponses.SERVICE_ERROR, "ServiceException"),
LambdaErrorResponses._construct_headers(exception_tuple[0]),
exception_tuple[1]
)

@staticmethod
def not_implemented_locally(message):
"""
Creates a Lambda Service NotImplementedLocally Response

Parameters
----------
message str
Message to be added to the body of the response

Returns
-------
Flask.Response
A response object representing the NotImplementedLocally Error
"""
exception_tuple = LambdaErrorResponses.NotImplementedException

return BaseLocalService.service_response(
LambdaErrorResponses._construct_error_response_body(LambdaErrorResponses.LOCAL_SERVICE_ERROR, message),
LambdaErrorResponses._construct_headers(exception_tuple[0]),
exception_tuple[1]
)

@staticmethod
def generic_path_not_found(*args):
"""
Creates a Lambda Service Generic PathNotFound Response

Parameters
----------
args list
List of arguments Flask passes to the method

Returns
-------
Flask.Response
A response object representing the GenericPathNotFound Error
"""
exception_tuple = LambdaErrorResponses.PathNotFoundException

return BaseLocalService.service_response(
LambdaErrorResponses._construct_error_response_body(
LambdaErrorResponses.LOCAL_SERVICE_ERROR, "PathNotFoundException"),
LambdaErrorResponses._construct_headers(exception_tuple[0]),
exception_tuple[1]
)

@staticmethod
def generic_method_not_allowed(*args):
"""
Creates a Lambda Service Generic MethodNotAllowed Response

Parameters
----------
args list
List of arguments Flask passes to the method

Returns
-------
Flask.Response
A response object representing the GenericMethodNotAllowed Error
"""
exception_tuple = LambdaErrorResponses.MethodNotAllowedException

return BaseLocalService.service_response(
LambdaErrorResponses._construct_error_response_body(LambdaErrorResponses.LOCAL_SERVICE_ERROR,
"MethodNotAllowedException"),
LambdaErrorResponses._construct_headers(exception_tuple[0]),
exception_tuple[1]
)

@staticmethod
def _construct_error_response_body(error_type, error_message):
"""
Constructs a string to be used in the body of the Response that conforms
to the structure of the Lambda Service Responses

Parameters
----------
error_type str
The type of error
error_message str
Message of the error that occured

Returns
-------
str
str representing the response body
"""
# OrderedDict is used to make testing in Py2 and Py3 consistent
return json.dumps(OrderedDict([("Type", error_type), ("Message", error_message)]))

@staticmethod
def _construct_headers(error_type):
"""
Constructs Headers for the Local Lambda Error Response

Parameters
----------
error_type str
Error type that occurred to be put into the 'x-amzn-errortype' header

Returns
-------
dict
Dict representing the Lambda Error Response Headers
"""
return {'x-amzn-errortype': error_type,
'Content-Type': 'application/json'}
Loading