-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Conversation
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 * Fix up tests to make them runable
Thanks for the detailed information in this PR. Really helps to set context. I will take a peek at the code. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
General idea looks good. I have some comments to improve readability and highlighted one bug.
lambda_response_dict = json.loads(lambda_response) | ||
|
||
if isinstance(lambda_response_dict, dict) and \ | ||
len(lambda_response_dict.keys()) == 3 and \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just the len(lambda_respons_dict)
will give you 3. Dont need keys()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
duh... :) will update
return lambda_response, lambda_logs | ||
is_lambda_user_error_response = False | ||
|
||
try: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you move this to a separate method for readability & testability? This whole blob is only to check if this is an error response.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can do :)
if isinstance(lambda_response_dict, dict) and \ | ||
len(lambda_response_dict.keys()) == 3 and \ | ||
'errorMessage' in lambda_response_dict and \ | ||
'errorType' in lambda_response_dict and \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
errorType
and stackTrace
are required. For example, this just returns {"errorMessage": "error" }
(I verified by calling Lambda). But if you returned a regular exception like TypeError
, then the other two fields are returned.
exports.handler = (event, context, callback) => {
// TODO implement
callback('error');
};
|
||
@staticmethod | ||
def resource_not_found(function_name): | ||
""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume you will fill in the doc strings later
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup. Didn't get that far in life yet :)
) | ||
|
||
@staticmethod | ||
def _service_response(body, headers, status_code): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this method already in the BaseService class? Can we use from there?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is and will combine this for the lambda_error_response
and the api gateway one as well.
return LambdaErrorResponses.invalid_request_content( | ||
"Could not parse request body into json: {}".format(str(json_error))) | ||
|
||
if flask_request.args: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a POST request. so query params are less of a problem. But anyway, good to handle.
|
||
log_type = flask_request.headers.get('X-Amz-Log-Type', 'None') | ||
if log_type != 'None': | ||
LOG.info("log-type: %s is not supported. Ignoring X-Amz-Log-Type header", log_type) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
debug
instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did this as info to display we are ignoring the X-Amz-Log-Type
header, it feels wrong to only display that to users when the --debug
is set. May be better to just exit there instead of still handling the request, which then it would make more sense to move this to debug.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think exiting and returning error would be better. It is explicit and less surprises.
if log_type != 'None': | ||
LOG.info("log-type: %s is not supported. Ignoring X-Amz-Log-Type header", log_type) | ||
|
||
invocation_type = flask_request.headers.get('X-Amz-Invocation-Type', 'RequestResponse') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Headers names are case sensitive?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They are not (just started with whatever AWS CLI sends). We have a CaseInsensitiveDictionary in the API GW class. Should be able to move that and use it here for all the header look ups.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would be helpful. Can you include that in your change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup. the move was easy :)
def _construct_error_handling(self): | ||
""" | ||
Updates the Flask app with Error Handlers for different Error Codes | ||
|
||
""" | ||
pass | ||
self._app.register_error_handler(500, LambdaErrorResponses.generic_service_exception()) | ||
self._app.register_error_handler(404, LambdaErrorResponses.resource_not_found("")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the downside of letting the FunctionNotFound
handle this? Having the function name there in response is useful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is for the generic 404, aka when a request is sent to an endpoint that does not exist (I called this out in the description above). I think it makes more sense to move this to being a path not found instead of resource not found.
|
||
lambda_response, lambda_logs = LambdaOutputParser.get_lambda_output(stdout_stream) | ||
if is_lambda_user_error_response: | ||
return self._service_response(lambda_response, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the response I got from Lambda Service on a sample invoke that failed. We are missing x-amzn-requestid
, date
, x-amz-executed-version
from these. Can we add them?
In fact, can we add a requestId header to every invocation?
{
"date": "Thu, 05 Jul 2018 20:24:47 GMT",
"content-type": "application/json",
"content-length": "114",
"connection": "keep-alive",
"x-amzn-requestid": "754e50c3-8091-11e8-afcc-a3b411a0cfac",
"x-amz-function-error": "Unhandled",
"x-amzn-remapped-content-length": "0",
"x-amz-executed-version": "$LATEST",
"x-amzn-trace-id": "root=1-5b3e7e8e-81e41598a657a40c4ad823a6;sampled=0"
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The lambda docker does not allow any requestId setting (it generates the uuid on the fly). So in order to make this experience clean, we would have to parse the output of the logs for the requestId and then set the header. That seems overkill. As of know, I am going to omit the requestId header.
- Adding
x-amz-executed-version
makes sense to me and will default to$LATEST
(since we do not support qualifiers). - Not sure what
x-amzn-remapped-content-length
means or how it is commuted, so was going to omit it. x-amzn-trace-id
only makes sense if we support XRay right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, let's omit requestId. How about setting x-amz-executed-version
to $LATEST
because there is no version locally?
Following up, what if they use AutoPublishAlias
and try to invoke the alias? I am okay if we don't support aliases now. Doesn't hurt.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From my response :):
Adding x-amz-executed-version makes sense to me and will default to $LATEST (since we do not support qualifiers).
Qualifiers might be easy to just add but we don't have a concept of a qualifier locally. I think I will hold off on adding the qualifier for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you answer the decode comment?
is_lambda_user_error_response = False | ||
|
||
try: | ||
lambda_response_dict = json.loads(lambda_response) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, you need to decode the lambda_response before json.loads it. Might be worth moving the decode to a upstream call somewhere. See this comment: #531 (comment)
…for is_lambda_error_response
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 🎉
* Add support for custom Log group in samcli * Added more unit test * Updated pr to use f string
* Add support for Custom Log Group (#532) * Add support for custom Log group in samcli * Added more unit test * Updated pr to use f string * SAM INIT support for Advanced Logging (#556) * SAM Init support for Advanced Logging * feature links added * SAM Init support for Advanced Logging * change class named from LoggingConfigTemplateModifier to LoggingConfigTemplateModifier * update file name to match structured_logging_template_modifier * Sam local Invoke support for Advanced logging (#552) * Sam local support for Advanced logging * Sam local support for Advanced logging * pass AWS_LAMBDA_LOG_FORMAT to runtime * remove unnecessary if statement * Update test_init_command.py --------- Co-authored-by: jonife <[email protected]>
Details:
Description of changes:
This is to handle the Error cases that can occur in the Local Lambda Service. This is meant to mimic the actually service with some caveats that are specific to local:
context': {'auth_type': None, 'client_region': u'us-west-2',
but Flask seems to swallow this part of the request and have not found a way to get at this information.Work In Progress
Following items are remaining:
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.