Skip to content

Commit

Permalink
feat(LocalLambdaService): Error handling for local lambda (aws#532)
Browse files Browse the repository at this point in the history
Details:
* Handling for when lambda returns an error
* Handled payload could not be parsed as JSON
* Abstracted if the lambda output is an error from the container into LambdaOutputParser
* Added handling for X-Amz-Log-Type
* Added handling for X-Amz-Invocation-Type
* Moved all Request validate to flask.before_request call
* Added handling for Query parameters (not supported)
* Added handling for unsupported_media_type
* Added handling for ServiceError
* Added handling for generic 404 Error handling
  • Loading branch information
jfuss authored and sanathkr committed Jul 9, 2018
1 parent 8fab30e commit 23ffd03
Show file tree
Hide file tree
Showing 10 changed files with 961 additions and 105 deletions.
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

0 comments on commit 23ffd03

Please sign in to comment.