"Internal Server Error" when querying of DynamoDB in Lambda using Python - python

I am currently trying to read my dynamodb table "saved_measurements" with the partition key "Name". I'm doing this through API Gateway Lambda proxy integration, and have tested my
event['pathParameters']['name'] to be working just fine so that shouldn't be the issue.
However, when I query my dynamodb table with my 'Name', the error appears.
{"message": "Internal server error"}
I have referenced many resources for querying dynamodb but nothing seems to work. I have also tried printing a example string within the response body like "yes" and it works with no issues. Only when I attempt to send my data in my response body do I meet that issue again.
What can I try to resolve this?
import json
import boto3
from boto3.dynamodb.conditions import Key, Attr
def lambda_handler(event, context):
client = boto3.resource('dynamodb')
table = client.Table('saved_measurements')
data = table.query(KeyConditionExpression=Key('Name').eq(event['pathParameters']['name']))
stuff = data.Items
response = {
"statusCode": 200,
"headers": {
"Content-Type": 'application/json',
"Access-Control-Allow-Origin" : "*",
"Access-Control-Allow-Headers" : "Content-Type",
"Access-Control-Allow-Methods": "OPTIONS,POST,GET"
},
"body": json.dumps(stuff),
"isBase64Encoded": False
}
return response

You code needs some optimising, however i'm not certain it will resolve the issue. Let me share how I would write it:
import json
import boto3
from boto3.dynamodb.conditions import Key, Attr
client = boto3.resource('dynamodb')
table = client.Table('saved_measurements')
def lambda_handler(event, context):
status_code=0
stuff = ""
try:
data = table.query(KeyConditionExpression=Key('Name').eq(event['pathParameters']['name']))
stuff = json.dumps(data['Items'])
status_code = 200
except Exception as e:
print(e)
stuff = e
status_code = 400
response = {
"statusCode": status_code,
"headers": {
"Content-Type": 'application/json',
"Access-Control-Allow-Origin" : "*",
"Access-Control-Allow-Headers" : "Content-Type",
"Access-Control-Allow-Methods": "OPTIONS,POST,GET"
},
"body": stuff,
"isBase64Encoded": False
}
return response
Some points on the code:
Use try/except block anytime you are making API requests to ensure you are not returning exceptions to your downstream tasks unexpectedly
I tend not to use dot notation in Python, hence I changed date.Items to data['Items']. It tends to work more often than dot notation.
Create you clients outside of your request handler in Lambda, this allows Lambda to reuse clients across multiple invocations which will improve your latency.
Don't return 200 for every request regardless on how it resulted, return the correct status code so your downstream processes don't get confused.
Give those changes a go, if you don't succeed, please share your APIGW config.

Related

Call Sagemaker endpoint invoke_endpoint in lambda function, question for request body formats

I have a deployed Sagemaker endpoint. When testing the endpoint using Predictor.predict, the endpoint works fine. I can pass down whichever Json format, it is able to process it correctly. However, I've been struggling calling endpoint from Lambda by using client.invoke_endpoint
I am trying to modify my request body to follow this format in this AWS documentation.
let request = {
// Instances might contain multiple rows that predictions are sought for.
"instances": [
{
// Request and algorithm specific inference parameters.
"configuration": {},
// Data in the specific format required by the algorithm.
"data": {
"<field name>": dataElement
}
}
]
}
I am not sure what should the configuration be, so this is what my request body looks like.
{
"instances": [
{
"data": {
"ID": "some ID",
"ACCOUNT": null,
"LEAD": some ID
"FORMNAME": "some Form",
"UTMMEDIUM": "some Medium",
"UTMSOURCE": "some Source"
}
},
{
"data": {
"ID": "some ID"
"ACCOUNT": "some ID"
"LEAD": null,
"FORMNAME": "some Form"
"UTMMEDIUM": null,
"UTMSOURCE": null
}
}
]
}
This is my Lambda function
import os
import io
import boto3
import json
# grab environment variables
ENDPOINT_NAME = 'xxxxx'
client = boto3.client('sagemaker-runtime')
def lambda_handler(event, context):
print("Received event: " + json.dumps(event, indent=2))
data = json.loads(json.dumps(event))
payload = str(data["instances"])
response = client.invoke_endpoint(EndpointName = ENDPOINT_NAME,
Body = payload,
ContentType = 'application/json',
Accept = 'application/json')
print(response)
return 'nothing'
I am able to invoke the Endpoint using the code above, but the endpoint kept having trouble processing the input. The input_fn in the endpoint looks like this
def input_fn(input_data, content_type):
if content_type == 'text/csv':
# Read the raw input data as CSV.
df = pd.read_csv(StringIO(input_data))
return df
elif content_type == 'application/json':
print('input_fn (elif): input_data')
print(input_data)
print(type(input_data))
print('input_fn (elif): input_data eval')
print(eval(input_data))
print('input_fn (elif): input_data eval type')
print(type(eval(input_data)))
df = pd.read_json(eval(input_data))
print('input_fn (elif): df.columns')
print(df.columns)
return df
else:
raise ValueError("{} not supported by script!".format(content_type))
The error I got isValueError: Invalid file path or buffer object type: <class 'list'>
The type of input_data is a string, and the type of eval(input_data) is a list.
I appreciate any insight! I've tried so many different things, including removing eval from my input_fn, or change pd.json_read to json.loads(json.dumps()) with pd.DataFrame.from_dict. I've gotten different errors like json.decoder.JSONDecodeError: Expecting value: Line Column 42 (column 42 is where the location of null), and unhashable type: 'dict'
I am really confused and not sure what to to next. Thank you!
This would involve debugging a bit more
But I recommend replacing instances of input_data with input_data[0] and also try decoding since it could be byte encoded. Maybe something like this, the reason for this kind of recommendation is described at bottom and might help, also instead of print use logging so that you can see input_data value in cloudwatch logs
input_data = (input_data[0].get('data') or input_data[0].get('body')).decode('utf-8')
Reason for recommendation:
In Sagemaker when using pytorch type models
the input_data is a list and each entry corresponds to data received from some request
The size of list will be number of request in it which also correspond to batch size in case it is using torchserve. By default batch_size is 1 in torchserve, so input_data is single element list.
also there is chance that you get byte encoded data
You might refer here also https://pytorch.org/serve/custom_service.html
This should help explain ValueError: Invalid file path or buffer object type: <class 'list'> and JSON decode error
The issue you are having is in this line
payload = str(data["instances"])
You've already serialized the data, so the following code block will do justice on the boto3 invocation side as you properly decode the response.
#event is the test request you are sending
data = json.loads(json.dumps(event))
payload = json.dumps(data)
response = runtime.invoke_endpoint(EndpointName=ENDPOINT_NAME,
ContentType='application/json',
Body=payload)
result = json.loads(response['Body'].read().decode()) #decode response
For processing the data you want appropriate exception handling when getting a non-JSON input. Use the following code to parse the request on the Flask end.
input_json = flask.request.get_json()
input = input_json['input'] #whatever your input json key is
result = model.predict(input)
# Transform predictions to JSON
result = {
'output': predictions
}
resultjson = json.dumps(result)
return flask.Response(response=resultjson, status=200, mimetype='application/json')
I am contributing this on behalf of my employer, AWS. My contribution is licensed under the MIT license. See here for a more detailed explanation.
https://aws-preview.aka.amazon.com/tools/stackoverflow-samples-license/

Fail AWS lambda in python without exception

I've written a small lambda function and connected it to the API Gateway. I added an error regex to match .*Error.* and return status 400 in the API Gateway. The problem I face is that the regex seems to match only if the lambda failed, as this thread suggests.
My lambda function:
import logging
import boto3
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
if int(event['event_id']) == 1:
return {
'statusCode': 200,
"status": "success"
}
elif int(event['event_id']) == 2:
return {
'statusCode': 400,
"status": "Error"
}
else:
raise Exception("Error")
It looks like case 1 works well, it returns status 200 by default. With event_id=3 it returns status 400 from the API Gateway (with the stack trace in the data, which I would like to avoid), but with event_id=2 it returns status 200 with the Error string in the data.
How can I mark the lambda as failed without throwing an exception?
A failed Lambda means there was some code section in Lambda Function that was failed to be executed. In the current code only the third path has such code section (artificially generated Exception).
It is not possible to execute all your code sections in Lambda and still fail.
If you want to handle API Gateway return status code via program, you'll have to use Lambda Integration. With Lambda integration you may return a response like:
{
"statusCode": "400",
body: json.dumps({ "error": "you messed up!" }),
headers: {
"Content-Type": "application/json",
}
}
and you'll get 400 in API response.

Dialogflow query HTTP Request

I am trying to create a very simple chatbot using dialogflow that I can ask a question and get a fulfillment message back. I was able to use python's dialogflow library to get this working, but when I tried to change it to a regular request it did not work. Here is the working code:
import os
import dialogflow
from google.api_core.exceptions import InvalidArgument
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = PATH_TO_JSON
DIALOGFLOW_PROJECT_ID = PROJECT_ID
DIALOGFLOW_LANGUAGE_CODE = 'en'
SESSION_ID = 'me'
text_to_be_analyzed = "How are my stocks"
session_client = dialogflow.SessionsClient()
session = session_client.session_path(DIALOGFLOW_PROJECT_ID, SESSION_ID)
text_input = dialogflow.types.TextInput(text=text_to_be_analyzed, language_code=DIALOGFLOW_LANGUAGE_CODE)
query_input = dialogflow.types.QueryInput(text=text_input)
try:
response = session_client.detect_intent(session=session, query_input=query_input)
except InvalidArgument:
raise
#print(response)
print("Response:", response.query_result.fulfillment_text)
and it prints
your stocks are good
Using the request library I tried a similar setup and wrote this:
my_key = CLIENT_ACCESS_TOKEN
url = "https://api.dialogflow.com/v1/query?v=20170712"
headers = {
'Authorization': 'Bearer ' + my_key ,
'Content-Type' : 'application/json'
}
body = {
"lang": "en",
"query": "how are my stocks",
"sessionId": "me",
}
r.post(url,headers=headers,data=body).text
and I get an error:
{
"status": {
"code": 400,
"errorType": "bad_request",
"errorDetails": "Cannot parse json. Please validate your json. Code: 400"
}
}
I am getting my example from this url for the query post request. The reason I want this to work as an http request is because I would like to be able to use it in other applications and want to have a consistent way of accessing my intents. Thanks for the help!
Upon more research I found the error at this link. So the question was less about integration with dialogflow and more about making a request in python.

HTTP Triggering Cloud Function with Cloud Scheduler

I have a problem with a job in the Cloud Scheduler for my cloud function. I created the job with next parameters:
Target: HTTP
URL: my trigger url for cloud function
HTTP method: POST
Body:
{
"expertsender": {
"apiKey": "ExprtSender API key",
"apiAddress": "ExpertSender APIv2 address",
"date": "YYYY-MM-DD",
"entities": [
{
"entity": "Messages"
},
{
"entity": "Activities",
"types":[
"Subscriptions"
]
}
]
},
"bq": {
"project_id": "YOUR GCP PROJECT",
"dataset_id": "YOUR DATASET NAME",
"location": "US"
}
}
The real values has been changed in this body.
When I run this job I got an error. The reason is caused by processing body from POST request.
However, when I take this body and use it as Triggering event in Testing I don't get any errors. So I think, that problem in body representation for my job but I havn't any idea how fix it. I'll be very happy for any idea.
Disclaimer:
I have tried to solve the same issue using NodeJS and I'm able to get a solution
I understand that this is an old question. But I felt like its worth to answer this question as I have spent almost 2 hours figuring out the answer for this issue.
Scenario - 1: Trigger the Cloud Function via Cloud Scheduler
Function fails to read the message in request body.
Scenario - 2: Trigger the Cloud Function via Test tab in Cloud Function interface
Function call always executes fine with no errors.
What did I find?
When the GCF routine is executed via Cloud Scheduler, it sends the header content-type as application/octet-stream. This makes express js unable to parse the data in request body when Cloud scheduler POSTs the data.
But when the exact same request body is used to test the function via the Cloud Function interface, everything works fine because the Testing feature on the interface sends the header content-type as application/json and express js is able to read the request body and parses the data as a JSON object.
Solution
I had to manually parse the request body as JSON (explicitly using if condition based on the content-type header) to get hold of data in the request body.
/**
* Responds to any HTTP request.
*
* #param {!express:Request} req HTTP request context.
* #param {!express:Response} res HTTP response context.
*/
exports.helloWorld = (req, res) => {
let message = req.query.message || req.body.message || 'Hello World!';
console.log('Headers from request: ' + JSON.stringify(req.headers));
let parsedBody;
if(req.header('content-type') === 'application/json') {
console.log('request header content-type is application/json and auto parsing the req body as json');
parsedBody = req.body;
} else {
console.log('request header content-type is NOT application/json and MANUALLY parsing the req body as json');
parsedBody = JSON.parse(req.body);
}
console.log('Message from parsed json body is:' + parsedBody.message);
res.status(200).send(message);
};
It is truly a feature issue which Google has to address and hopefully Google fixes it soon.
Cloud Scheduler - Content Type header issue
Another way to solve the problem is this:
request.get_json(force=True)
It forces the parser to treat the payload as json, ingoring the Mimetype.
Reference to the flask documentation is here
I think this is a bit more concise then the other solutions proposed.
Thank you #Dinesh for pointing towards the request headers as a solution! For all those who still wander and are lost, the code in python 3.7.4:
import json
raw_request_data = request.data
# Luckily it's at least UTF-8 encoded...
string_request_data = raw_request_data.decode("utf-8")
request_json: dict = json.loads(string_request_data)
Totally agree, this is sub-par from a usability perspective. Having the testing utility pass a JSON and the cloud scheduler posting an "application/octet-stream" is incredibly irresponsibly designed.
You should, however, create a request handler, if you want to invoke the function in a different way:
def request_handler(request):
# This works if the request comes in from
# requests.post("cloud-function-etc", json={"key":"value"})
# or if the Cloud Function test was used
request_json = request.get_json()
if request_json:
return request_json
# That's the hard way, i.e. Google Cloud Scheduler sending its JSON payload as octet-stream
if not request_json and request.headers.get("Content-Type") == "application/octet-stream":
raw_request_data = request.data
string_request_data = raw_request_data.decode("utf-8")
request_json: dict = json.loads(string_request_data)
if request_json:
return request_json
# Error code is obviously up to you
else:
return "500"
One of the workarounds that you can use is to provide a header "Content-Type" set to "application/json". You can see a setup here.

Facebook Messenger with Flask

I'm trying to get the FB messenger API working using Python's Flask, adapting the following instructions: https://developers.facebook.com/docs/messenger-platform/quickstart
So far, things have been going pretty well. I have verified my callback and am able to receive the messages I send using Messenger on my page, as in the logs in my heroku server indicate the appropriate packets of data are being received by my server. Right now I'm struggling a bit to send responses to the client messenging my app. In particular, I am not sure how to perform the following segment from the tutorial in Flask:
var token = "<page_access_token>";
function sendTextMessage(sender, text) {
messageData = {
text:text
}
request({
url: 'https://graph.facebook.com/v2.6/me/messages',
qs: {access_token:token},
method: 'POST',
json: {
recipient: {id:sender},
message: messageData,
}
}, function(error, response, body) {
if (error) {
console.log('Error sending message: ', error);
} else if (response.body.error) {
console.log('Error: ', response.body.error);
}
});
}
So far, I have this bit in my server-side Flask module:
#app.route('/', methods=["GET", "POST"])
def chatbot_response():
data = json.loads(req_data)
sender_id = data["entry"][0]["messaging"][0]["sender"]["id"]
url = "https://graph.facebook.com/v2.6/me/messages"
qs_value = {"access_token": TOKEN_OMITTED}
json_response = {"recipient": {"id": sender_id}, "message": "this is a test response message"}
response = ("my response text", 200, {"url": url, "qs": qs_value, "method": "POST", "json": json_response})
return response
However, running this, I find that while I can process what someone send my Page, it does not send a response back (i.e. nothing shows up in the messenger chat box). I'm new to Flask so any help would be greatly appreciated in doing the equivalent of the Javascript bit above in Flask.
Thanks!
This is the code that works for me:
data = json.loads(request.data)['entry'][0]['messaging']
for m in data:
resp_id = m['sender']['id']
resp_mess = {
'recipient': {
'id': resp_id,
},
'message': {
'text': m['message']['text'],
}
}
fb_response = requests.post(FB_MESSAGES_ENDPOINT,
params={"access_token": FB_TOKEN},
data=json.dumps(resp_mess),
headers = {'content-type': 'application/json'})
key differences:
message needs a text key for the actual response message, and you need to add the application/json content-type header.
Without the content-type header you get the The parameter recipient is required error response, and without the text key under message you get the param message must be non-empty error response.
This is the Flask example using fbmq library that works for me:
echo example :
from flask import Flask, request
from fbmq import Page
page = fbmq.Page(PAGE_ACCESS_TOKEN)
#app.route('/webhook', methods=['POST'])
def webhook():
page.handle_webhook(request.get_data(as_text=True))
return "ok"
#page.handle_message
def message_handler(event):
page.send(event.sender_id, event.message_text)
In that scenario in your tutorial, the node.js application is sending an HTTP POST request back to Facebook's servers, which then forwards the content on to the client.
So far, sounds like your Flask app is only receiving (AKA serving) HTTP requests. The reason is that that's what the Flask library is all about, and it's the only thing that Flask does.
To send an HTTP request back to Facebook, you can use any Python HTTP client library you like. There is one called urllib in the standard library, but it's a bit clunky to use... try the Requests library.
Since your request handler is delegating to an outgoing HTTP call, you need to look at the response to this sub-request also, to make sure everything went as planned.
Your handler may end up looking something like
import json
import os
from flask import app, request
# confusingly similar name, keep these straight in your head
import requests
FB_MESSAGES_ENDPOINT = "https://graph.facebook.com/v2.6/me/messages"
# good practice: don't keep secrets in files, one day you'll accidentally
# commit it and push it to github and then you'll be sad. in bash:
# $ export FB_ACCESS_TOKEN=my-secret-fb-token
FB_TOKEN = os.environ['FB_ACCESS_TOKEN']
#app.route('/', method="POST")
def chatbot_response():
data = request.json() # flasks's request object
sender_id = data["entry"][0]["messaging"][0]["sender"]["id"]
send_back_to_fb = {
"recipient": {
"id": sender_id,
},
"message": "this is a test response message"
}
# the big change: use another library to send an HTTP request back to FB
fb_response = requests.post(FB_MESSAGES_ENDPOINT,
params={"access_token": FB_TOKEN},
data=json.dumps(send_back_to_fb))
# handle the response to the subrequest you made
if not fb_response.ok:
# log some useful info for yourself, for debugging
print 'jeepers. %s: %s' % (fb_response.status_code, fb_response.text)
# always return 200 to Facebook's original POST request so they know you
# handled their request
return "OK", 200
When doing responses in Flask, you have to be careful. Simply doing a return statement won't return anything to the requester.
In your case, you might want to look at jsonify(). It will take a Python dictionary and return it to your browser as a JSON object.
from flask import jsonify
return jsonify({"url": url, "qs": qs_value, "method": "POST", "json": json_response})
If you want more control over the responses, like setting codes, take a look at make_response()

Categories