21

I'm in the process of rebuilding a PHP app in Node.js on top of the Express framework.

One part of the application is a callback url that an Amazon SNS notification is posted to.

The POST body from SNS is currently read in the following way in PHP (which works):

$notification = json_decode(file_get_contents('php://input'));

In Express I have tried the following:

app.post('/notification/url', function(req, res) {
    console.log(req.body);
});

However, watching the console, this only logs the following when the post is made:

{}

So, to repeat the question: How do you access an Amazon SNS post body with Express / Node.js

timstermatic
  • 1,710
  • 14
  • 32
  • 1
    As context: the reason why this (reasonable, naive) approach fails is that SNS HTTP sets. "Content-Type: text/plain; charset=UTF-8" See: http://docs.aws.amazon.com/sns/latest/dg/SendMessageToHttp.html (SNS doesn't really grok JSON) – Jason Mar 07 '16 at 02:06

5 Answers5

44

Another approach would be to fix the Content-Type header.

Here is middleware code to do this:

exports.overrideContentType = function(){
  return function(req, res, next) {
    if (req.headers['x-amz-sns-message-type']) {
        req.headers['content-type'] = 'application/json;charset=UTF-8';
    }
    next();
  };
}

This assumes there is a file called util.js located in the root project directory with:

util = require('./util');

in your app.js and invoked by including:

app.use(util.overrideContentType());

BEFORE

app.use(express.bodyParser());

in the app.js file. This allows bodyParser() to parse the body properly...

Less intrusive and you can then access req.body normally.

Paul
  • 456
  • 5
  • 2
  • 3
    This is a great answer, especially if you want to use the same app to process the notifications and subscription confirmations from SNS. – supaseca Jan 03 '15 at 01:06
  • http://docs.aws.amazon.com/sns/latest/dg/SendMessageToHttp.html#SendMessageToHttp.prepare Is there a reason AWS uses `text/plain` as the Content-Type? It was a real headache dealing with this. Glad there was a solution out there – lasec0203 Nov 05 '17 at 05:16
  • Look at [this topic](https://forums.aws.amazon.com/thread.jspa?threadID=69413) and draw your own conclusions. Watch out for the dates. Instead of trying to find alternatives to work around the problem, it would be better for AWS to fix it. – TaoTao Dec 13 '17 at 20:20
  • You can now have Amazon SNS set the right `Content-Type` header to `application/json`. To achieve that, modify the `DeliveryPolicy` attribute of your Amazon SNS subscription, setting the `headerContentType` property to `application/json`, or any other value supported. You can find all values supported here: https://docs.aws.amazon.com/sns/latest/dg/sns-message-delivery-retries.html#creating-delivery-policy – Otavio Ferreira Mar 27 '23 at 18:48
5

This is based on AlexGad's answer.Particularly this comment:

The standard express parser will only handle application/json, application/x-www-form-encoded and multipart/form-data. I added some code above to place before your body parser.

app.post('/notification/url', function(req, res) {
    var bodyarr = []
    req.on('data', function(chunk){
      bodyarr.push(chunk);
    })  
    req.on('end', function(){
      console.log( bodyarr.join('') )
    })  
})
timstermatic
  • 1,710
  • 14
  • 32
  • Technically correct, the higher voted post is the "cleaner" solution of the named problem. Let the well tested framework do the job... – andreas Jul 05 '16 at 07:44
4

Take a look at AWS Node.js SDK - it can access all AWS service endpoints.

    var sns = new AWS.SNS();

    // subscribe
    sns.subscribe({topic: "topic", Protocol: "https"}, function (err, data) {
      if (err) {
        console.log(err); // an error occurred
      } else {
        console.log(data); // successful response - the body should be in the data
     }
   });


    // publish example
    sns.publish({topic: "topic", message: "my message"}, function (err, data) {
      if (err) {
        console.log(err); // an error occurred
      } else {
        console.log(data); // successful response - the body should be in the data
     }
   });

EDIT: The problem is that the standard body parser does not handle plain/text which is what SNS sends as the content type. Here is code to extract the raw body. Place it before your body parser:

app.use(function(req, res, next) {
    var d= '';
    req.setEncoding('utf8');
    req.on('d', function(chunk) { 
        d+= chunk;
    });
    req.on('end', function() {
        req.rawBody = d;
        next();
    });
});

You can then use:

JSON.stringify(req.rawBody));

within your route to create a javascript object and operate on the SNS post appropriately.

You could also modify the body parser to handle text/plain but its not a great idea to modify middleware. Just use the code above.

Sagiv Ofek
  • 25,190
  • 8
  • 60
  • 55
AlexGad
  • 6,612
  • 5
  • 33
  • 45
  • I think this may be a stage earlier. My application is already subscribed. SNS does post to it as I can see it in the console, but I can't access the body of the post. – timstermatic Aug 28 '13 at 14:01
  • 1
    Ah got it, sorry. I understand now what you are doing. I believe SNS sends the content as text/plain even though the text is in JSON format. How have you setup your body parsers? The standard express parser will only handle application/json, application/x-www-form-encoded and multipart/form-data. I added some code above to place before your body parser. It will give you a rawbody from your request which you can test to insure that you are getting data from SNS. Log this and then you may have to write your own body parser. – AlexGad Aug 28 '13 at 17:01
  • I couldn't get this to work. However it did point me in the right direction with req.on and chunking so thank you for the input. I've put my solution as the accepted answer. – timstermatic Aug 28 '13 at 19:11
1

Here is how you can do that supposing that you're using body-parser.

Just add the following lines to your app.js:

app.use(bodyParser.json());
app.use(bodyParser.text({ type: 'text/plain' }));

This information can also be found in the body-parser official document:
https://github.com/expressjs/body-parser

  • +1 Adding both the options worked for my use case because I need json for sending mail and from AWS bounce back returns text/plain type. – Sachin Tanpure Jul 12 '21 at 09:17
0

The problem here is that Amazon SNS sets the Content-Type header to text/plain by default. Now, there's a built-in solution in Amazon SNS, which just launched support for custom Content-Type headers for HTTP messages delivered from topics. Here's the launch post: https://aws.amazon.com/about-aws/whats-new/2023/03/amazon-sns-content-type-request-headers-http-s-notifications/

You'll have to modify the DeliveryPolicy attribute of your Amazon SNS subscription, setting the headerContentType property to application/json, or any other value supported. You can find all values supported here: https://docs.aws.amazon.com/sns/latest/dg/sns-message-delivery-retries.html#creating-delivery-policy

{
    "healthyRetryPolicy": {
        "minDelayTarget": 1,
        "maxDelayTarget": 60,
        "numRetries": 50,
        "numNoDelayRetries": 3,
        "numMinDelayRetries": 2,
        "numMaxDelayRetries": 35,
        "backoffFunction": "exponential"
    },
    "throttlePolicy": {
        "maxReceivesPerSecond": 10
    },
    "requestPolicy": {
        "headerContentType": "application/json"
    }
}

You set the DeliveryPolicy attribute by calling either the Subscribe or the SetSubscriptionAttributes API action:

Alternatively, you can use AWS CloudFormation for setting this policy as well, using the AWS::SNS::Subscription resource.

Otavio Ferreira
  • 755
  • 6
  • 11