I have stumbled upon a rather strange and, to my knowledge, undocumented behaviour of Amazon SNS. I am looking for a solution or settings to fix it.
SUMMARY
I have a SNS Topic, with an HTTPS subscription pointing to an Amazon API Gateway REST endpoint, backed with a Node.js Lambda function for executing the request.
Now, if I use SNS and Publish on the topic , the whole API Gateway mapping template gets ignored/short-circuited. The Lambda function ends-up receiving ONLY the original SNS JSON object.
However, if I use a web browser (or curl) to access the endpoint the API Gateway Mapping Translation gets called and the proper JSON data gets passed onto the Lambda function.
THE API Gateway Endpoint
The API Gateway (aka TheApi hereafter) is created with a sms resource under which a "path parameters" {phone}. Therefore, you can query https://TheApi/sms/111-222-3333 with either a POST or GET method.
Both methods have a generic Mapping Template which grabs all paths parameters, all headers parameters, all query parameters and the whole request body and translate that into one LARGE request body JSON object. Here is what the template looks like:
{
"resource-path" : "$context.resourcePath",
"http-method" : "$context.httpMethod",
"headers": {
#foreach($param in $input.params().header.keySet())
"$param": "$util.escapeJavaScript($input.params().header.get($param))" #if($foreach.hasNext),#end
#end
},
"query": {
#foreach($param in $input.params().querystring.keySet())
"$param": "$util.escapeJavaScript($input.params().querystring.get($param))" #if($foreach.hasNext),#end
#end
},
"paths": {
#foreach($param in $input.params().path.keySet())
"$param": "$util.escapeJavaScript($input.params().path.get($param))" #if($foreach.hasNext),#end
#end
},
"body" : $input.json('$')
}
This resulting object is then fed to the Lambda function as the event on which the lambda function operates. Here is the result of a simple API Gateway "Test":
Tue Feb 09 00:54:13 UTC 2016 : Endpoint request body after transformations:
{
"resource-path" : "/sms/{phone}",
"http-method" : "POST",
"headers": {
},
"query": {
},
"paths": {
"phone": "111-222-3333"
},
"body" : {"foo":"bar","Alice":"Bob"}
}
This endpoint and the called Lambda function work flawlessly when called from a Web Browser (or a curl call ). AWS Cloud Watch logs show that everything is good under the sun, the Lambda event received is the same as above, therefore the Mapping translation is called.
THE Problem
Now, if I use SNS and Publish on the topic (the one with the HTTPS subscription on the API Gateway endpoint listed at the top), the whole API Gateway mapping template gets ignored/short-circuited.
The Lambda function ends-up receiving ONLY the original SNS JSON object and none of that custom mapping I wrote. The Lambda function doesn't receive any information about the calling agent, the requested url, the headers.... nada!! Here what the Lambda event looks like, as shown in CloudWatch:
{
"Type": "Notification",
"MessageId": "d38077e1-406a-5122-8a57-38cecfc635fd",
"TopicArn": "arn:aws:sns:us-east-1:...:...",
"Subject": "Ceci est un test",
"Message": "Ceci est un message de test.",
"Timestamp": "2016-02-06T06:06:36.649Z",
"SignatureVersion": "1",
"Signature": "...",
"SigningCertURL": "...",
"MessageAttributes": {
"AWS.SNS.MOBILE.MPNS.Type": {
"Type": "String",
"Value": "token"
},
"AWS.SNS.MOBILE.MPNS.NotificationClass": {
"Type": "String",
"Value": "realtime"
},
"AWS.SNS.MOBILE.WNS.Type": {
"Type": "String",
"Value": "wns/badge"
}
}
}
As it can be seen, this JSON object is completely different.
FOOD for thoughts
Some may wonder: "Why I go to the trouble of making the API Gateway when I could forward SNS events directly to the Lambda functions?". The reason is quite simple, I need to attach additional information with the SNS message, in this case the phone number to send the message to. Using API Gateway I can create as many subscriptions to as many phone numbers without duplicating any code.
Others may wonder: "Why not using the SMS subscription built into SNS instead of making my own?". For one thing, I'm in Canada and Amazon SMS subscriptions no longer works in Canada. Secondly, I may wish to use another SMS service that Amazon's.
As it turns out, SNS topics can call Lambda functions directly. In which case, the SNS JSON object is exactly the same. Thus, it is as though AWS is detecting the HTTPS endpoint domain, resolving the underlying Lambda function and routing the call directly to the Lambda function without passing through the API Gateway services.
In fact, when I build another REST endpoint on another domain I control, I indeed receive the POST request with the SNS JSON body, which I can forward to the API Gateway endpoint and it gets translated just well.
Just like this:
{
"resource-path": "/sms/{phone}",
"http-method": "POST",
"headers": {
"Accept": "*/*",
"CloudFront-Forwarded-Proto": "https",
"CloudFront-Is-Desktop-Viewer": "true",
"CloudFront-Is-Mobile-Viewer": "false",
"CloudFront-Is-SmartTV-Viewer": "false",
"CloudFront-Is-Tablet-Viewer": "false",
"CloudFront-Viewer-Country": "US",
"Content-Type": "application/json",
"Via": "1.1 c903e93e57c533ecd52152e4407a295e.cloudfront.net (CloudFront)",
"X-Amz-Cf-Id": "Fy_dCf5yJbW1GOZWJMVJqhbz1qt6sLfNO0N33FqAtf56X1tB4py8Ig==",
"X-Forwarded-For": "69.65.27.156, 54.182.212.5",
"X-Forwarded-Port": "443",
"X-Forwarded-Proto": "https"
},
"query": {},
"paths": {
"phone": "14184901585"
},
"body": {
"Type": "Notification",
"MessageId": "d38077e1-406a-5122-8a57-38cecfc635fd",
"TopicArn": "arn:aws:sns:us-east-1:...:...",
"Subject": "Ceci est un test",
"Message": "Ceci est un message de test.",
"Timestamp": "2016-02-06T06:06:36.649Z",
"SignatureVersion": "1",
"Signature": "...",
"SigningCertURL": "...",
"UnsubscribeURL": "...",
"MessageAttributes": {
"AWS.SNS.MOBILE.MPNS.Type": {
"Type": "String",
"Value": "token"
},
"AWS.SNS.MOBILE.MPNS.NotificationClass": {
"Type": "String",
"Value": "realtime"
},
"AWS.SNS.MOBILE.WNS.Type": {
"Type": "String",
"Value": "wns/badge"
}
}
}
}
CALL for help
Are there any hidden settings somewhere when I can make this SNS -> API Gateway -> Lambda work with the proper Mapping Translation?
The mapping template is applied based on the request's content type. If there is no content type specified in the request, it defaults to 'application/json'.
Based on your description, I would assume that your mapping template is set up with the content type 'application/json'. This works fine as long as the client doesn't specify a different content type in its request (which is the case for browsers for example).
Since SNS sends the request with a header 'Content-type: text/plain' (SNS Send Message Over HTTP), it doesn't match your mapping template's content type and hence will ignore it. To get working you could either change the content type in your current mapping or add another one which matches 'text/plain'.
For some more details you could also have a look here in the AWS forums: Default Content-Type for Mapping Template
Best,
Jurgen
Related
I'm currently using SNS to fan out messages to a few SQS queues.
Trying to parse the SQS message and was wondering if there is an out of the box message model to parse the queue message body in to because it's a SNS notification.
Is there such out-of-the-box model available for this kind of scenario?
E.g.: For S3+SNS events -> com.amazonaws.services.s3.event.S3EventNotification
I checked the SQS/ SNS Java SDKs and couldn't find anything similar.
Does this mean the model has to be built in the application code to parse such message bodies?
E.g.:
In the below example queue message, "Body" section is a SNS notification where as I'm interested in "Body -> "Message" section. Can the "Body" be parsed in to an out-of-the-box AWS model in this case?
{
"Body": {
"Type": "Notification",
"MessageId": "272a7e6b-ea5e-46c3-991f-3563d7cd3f09",
"Token": null,
"TopicArn": "arn:aws:sns:us-east-1:000000000000:contact-subscription-topic",
"Message": {
"type": "contactSubInstantiationMessage"
},
"SubscribeURL": null,
"Timestamp": "2020-10-20T03:54:14.022Z",
"SignatureVersion": "1",
"Signature": "EXAMPLEpH+..",
"SigningCertURL": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-0000000000000000000000.pem"
},
"Attributes": {
"ApproximateFirstReceiveTimestamp": "1603166057169",
"SenderId": "AIDAIT2UOQQY3AUEKVGXU",
"ApproximateReceiveCount": "1",
"SentTimestamp": "1603166054040"
},
"ReceiptHandle": "ibetdkwxaxkqzjxhrkqtgtbrneyylminkvatzwcounxxnubhdktzzkdqrgzxqsebrdfuuxpwnhbuyhvrcbrwxbfgvgdekcygsgauxtcmouzzhlyqvaazkpqmvmmpixbhnpfpldlgjzcnkmaupbikegthoqvdmxyjcvetpisdzxpxrtsrtxvpbmyln",
"MD5OfBody": "9657ff8451167353e3d11c492d99d15f",
"MessageId": "879b6742-8006-bef5-d233-f7b8c8bb33d7"
}
Appreciate your thoughts on this.
Thanks team!
The following post describes a way to eliminate the above mentioned SNS metadata by setting "RawMessageDelivery" to "true" for the subscription;
Amazon SNS -> SQS message body
https://docs.aws.amazon.com/sns/latest/dg/sns-large-payload-raw-message-delivery.html
Marking the issue as resolved.
Thanks.
I created an AWS Lambda Application API using AWS Toolkit for .Net Core 3.1. It has 2 Get request that expecting text JSON in a request body and returning text JSON as an output. It does not require any database connection or any other AWS resources. Locally everything works fine, all tests are passing. I publish my app to AWS account using AWS Toolkit which runs Cloud Formation setting file, again no problems, all passing. This creates my AWS Lambda API app with my API endpoint. However, when I try to use I am getting "403 Forbidden" errors:
Other thing I notice is that the default API Gateway type is Edge, I am unsure if that's making a problem. I would like to set it up to Private in cloud formation stuck from .Net Core level. I assume it is something to be change here:
{
"AWSTemplateFormatVersion": "2010-09-09",
"Transform": "AWS::Serverless-2016-10-31",
"Description": "An AWS Serverless Application that uses the ASP.NET Core framework running in Amazon Lambda.",
"Resources": {
"AspNetCoreFunction": {
"Type": "AWS::Serverless::Function",
"Properties": {
"Handler": "AES.Protocol::AES.Protocol.LambdaEntryPoint::FunctionHandlerAsync",
"Runtime": "dotnetcore3.1",
"CodeUri": "",
"MemorySize": 256,
"Timeout": 30,
"Role": null,
"Policies": [
"AWSLambdaFullAccess"
],
"Events": {
"ProxyResource": {
"Type": "Api",
"Properties": {
"Path": "/{proxy+}",
"Method": "ANY"
}
},
"RootResource": {
"Type": "Api",
"Properties": {
"Path": "/",
"Method": "ANY"
}
}
}
}
}
},
"Outputs": {
"ApiURL": {
"Description": "API endpoint URL for Prod environment",
"Value": {
"Fn::Sub": "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/"
}
}
}
}
The previous question related to this API might be helpful.
I manage to find a solution. It seems like CloudFront is not supporting GET request with "body". So changing "GET" to "POST" request fix the problem.
I'm trying to create an endpoint on Api Gateway that writes on AWS s3 bucket using a filename like data/$timestamp where timestamp is the $context.requestTimeEpoch variable that should be available in each request
My api gateway request integration is this
{
"type": "AWS",
"httpMethod": "PUT",
"uri": "arn:aws:apigateway:eu-west-1:bucketname.s3:path/data/{reqTimestamp}",
"credentials": "arn:aws:iam::xxxxx:role/ApiGatewayWriteToDemoS3Bucket",
"requestParameters": {
"integration.request.path.reqTimestamp": "context.requestTime"
},
"passthroughBehavior": "WHEN_NO_MATCH",
"timeoutInMillis": 29000,
"cacheNamespace": "5f92eajcg9",
"cacheKeyParameters": [],
"integrationResponses": {
"200": {
"statusCode": "200",
"responseParameters": {
"method.response.header.Access-Control-Allow-Origin": "'*'"
},
"responseTemplates": {
"application/json": null
}
}
}
}
However I get this error
Execution failed due to configuration error: Illegal character in path at index 49: https://bucketname.s3-eu-west-1.amazonaws.com/data/{reqTimestamp}
it worked only once, then I had an S3 authentication error but that's another story
You need to add a value in the URL Path Parameter configuration of the Integration Request in your API Gateway resource method, setting the name to reqTimestamp, and value to method.request.path.reqTimestamp.
This will map the value received by API Gateway in the URL to the path override parameter you defined.
I am trying to create a template so that when i call api/divide/inputvalue, The api sends back response from DynamoDB which corresponds to inputvalue mapping.
Its pretty straight forward since i am fetching value directly from db without any business logic hence I don't need any lambda. But all the examples that I google or all tutorials they are using lambdas and i am now lost that how can i make it working without lambda
This is what I have so far. There is bug in this template right now since I haven't provided Uri in ApiGateway::Method. Which is what I am currently stuck at.
{
"AWSTemplateFormatVersion": "2010-09-09",
"Resources": {
"Deployment": {
"Type": "AWS::ApiGateway::Deployment",
"Properties": {
"RestApiId": { "Ref": "restApiName" },
"Description": "First Deployment",
"StageName": "StagingStage"
},
"DependsOn" : ["restApiMethod"]
},
"restApiMethod": {
"Type": "AWS::ApiGateway::Method",
"Properties": {
"AuthorizationType": "NONE",
"HttpMethod": "GET",
"ResourceId": {"Ref": "apiRestResource"},
"RestApiId": {"Ref": "restApiName"},
"Integration": {
"Type": "AWS",
"IntegrationHttpMethod": "GET",
"IntegrationResponses": [{"StatusCode": 200}],
"Uri": { "Fn::Sub":"arn.aws.apigateway:${AWS::Region}:dynamodb:action/${restApiName.Arn}"}
},
"MethodResponses": [{"StatusCode": 200}]
},
"DependsOn": ["apiRestResource"]
},
"apiRestResource": {
"Type": "AWS::ApiGateway::Resource",
"Properties": {
"RestApiId": {"Ref": "restApiName"},
"ParentId": {
"Fn::GetAtt": ["restApiName","RootResourceId"]
},
"PathPart": "divide"
},
"DependsOn": ["restApiName"]
},
"restApiName": {
"Type": "AWS::ApiGateway::RestApi",
"Properties": {
"Name": "CalculationApi"
}
}
}
}
According to the documentation, the Uri property is structured as follows for AWS service-proxy integration types:
If you specify AWS for the Type property, specify an AWS service that follows the form: arn:aws:apigateway:region:subdomain.service|service:path|action/service_api. For example, a Lambda function URI follows the form: arn:aws:apigateway:region:lambda:path/path. The path is usually in the form /2015-03-31/functions/LambdaFunctionARN/invocations. For more information, see the uri property of the Integration resource in the Amazon API Gateway REST API Reference.
The uri API Gateway property reference provides more details:
For AWS integrations, the URI should be of the form arn:aws:apigateway:{region}:{subdomain.service|service}:{path|action}/{service_api}. Region, subdomain and service are used to determine the right endpoint. For AWS services that use the Action= query string parameter, service_api should be a valid action for the desired service. For RESTful AWS service APIs, path is used to indicate that the remaining substring in the URI should be treated as the path to the resource, including the initial /.
For an AWS service proxy to the dynamodb service calling the Query Action, the Uri should be something like this (using the YAML short-form of Fn::Sub to insert a Ref for the current AWS region):
!Sub "arn:aws:apigateway:${AWS::Region}:dynamodb:action/Query"
As for your broader use-case of using API Gateway to access DynamoDB without using Lambda functions, refer to Andrew Baird's tutorial blog post, "Using Amazon API Gateway as a Proxy for DynamoDB", and translate the specified Management Console steps to corresponding CloudFormation template resources.
I am trying to POST a json string to API Gateway and in turn have API Gateway send the JSON to an EC2 server.
My issue is I can't find good documentation from Amazon on how to accomplish this.
When I test the setup I get this
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Response><Errors><Error><Code>InvalidHttpRequest</Code><Message>The HTTP request is invalid. Reason: Unable to parse request</Message></Error></Errors><RequestID>1fa47f52-d75c-4ff8-8992-3eac11a79015</RequestID></Response>"
Which means very little to me. I assume it is an issue with API Gateway trying to send the request to EC2 and it can't so it generates this error. So perhaps I am setting up the EC2 AWS Service Proxy in API Gateway incorrectly. Which is likely because I have no idea what I am supposed to set 'Action' to right now I have it pointing to the EC2 instance, only cause i don't see any other place to put that info.
This really shouldn't be that hard I have successfully done this thing connecting to Lambda and have looked through all the documentation and all I can find is this: http://docs.aws.amazon.com/apigateway/latest/developerguide/getting-started-aws-proxy.html#getting-started-aws-proxy-add-resources
Which is less than helpful for this scenario. Any Ideas?
I think you confused AWS Service Proxy and HTTP Service proxy.
API Gateway can forward API calls to different type of backends:
- a lambda function
- an AWS Service (see http://docs.aws.amazon.com/apigateway/latest/developerguide/integrating-api-with-aws-services-s3.html for an example)
- an existing API, running on AWS or on premises (your use case)
When defining you API, be sure to define a POST verb and point the Endpoint URL to your EC2 instance URL
I just made a test using the JSON POST service available online at http://gurujsonrpc.appspot.com/ and it works as expected.
Here is the Swagger export of my test API.
{
"swagger": "2.0",
"info": {
"version": "2016-04-11T20:46:13Z",
"title": "test"
},
"host": "c22wfjg4d7.execute-api.eu-west-1.amazonaws.com",
"basePath": "/prod",
"schemes": [
"https"
],
"paths": {
"/": {
"post": {
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "200 response",
"schema": {
"$ref": "#/definitions/Empty"
}
}
},
"x-amazon-apigateway-integration": {
"responses": {
"default": {
"statusCode": "200"
}
},
"uri": "http://gurujsonrpc.appspot.com/guru",
"httpMethod": "POST",
"type": "http"
}
}
}
},
"definitions": {
"Empty": {
"type": "object"
}
}
}