Serverless-express: Async handler doesn't work on node 8.10

Created on 4 Apr 2018  路  36Comments  路  Source: vendia/serverless-express

I have reduced my project to that snippet which generates an error using the new 8.10:

import awsServerlessExpress from 'aws-serverless-express'
import express from 'express'

export default async (event, context, callback) => {
  const app = express()

  app.get('/', function(req, res){
    res.send('hello world')
  })

  const server = awsServerlessExpress.createServer(app)

  return awsServerlessExpress.proxy(server, event, context)
}

Babel compilation targets node 8.10 to be sure it doesn't convert async/await to native js code.

Cloudwatch reports:

START RequestId: 3cb224ae-3810-11e8-ba0f-437e442fcf6a Version: $LATEST
Unable to stringify response body as json: Converting circular structure to JSON: TypeError

END RequestId: 3cb224ae-3810-11e8-ba0f-437e442fcf6a
REPORT RequestId: 3cb224ae-3810-11e8-ba0f-437e442fcf6a  Duration: 30027.56 ms   Billed Duration: 30000 ms   Memory Size: 512 MB Max Memory Used: 31 MB  

Removing the async in the handler fix the problem. But my whole project does some await in the handler which requires the handler to be async.

enhancement

Most helpful comment

PR for this here https://github.com/awslabs/aws-serverless-express/pull/173. Note that I plan on improving the interface in a future breaking change (aws-serverless-express 4.0.0), but for now this is a backwards compatible change with the following interface:

exports.handler = async (event, context) => awsServerlessExpress.proxy(server, event, context, 'PROMISE').promise

All 36 comments

You don't need to add async here. This will force awsServerlessExpress.proxy to return as a Promise. This library is then trying to JSON.stringify that promise. If you remove async from your handler and it works, you're good to go.

Feel free to re-open if there's still an issue.

I know that removing the async fix the problem.
As I said, I've removed a lot of codes from my project to keep only what makes the lib to fail.

What if I have:

import awsServerlessExpress from 'aws-serverless-express'
import express from 'express'
import configureApp from './configureApp'

export default async (event, context, callback) => {
  const app = express()

  await configureApp(app)

  app.get('/', function(req, res){
    res.send('hello world')
  })

  const server = awsServerlessExpress.createServer(app)

  return awsServerlessExpress.proxy(server, event, context)
}

Where configureApp.js is:

import massive from 'massive'
import nconf from 'nconf'

const pgConfig = nconf.get('database').postgresql

export default async (application) => {
  const postgres = await massive(pgConfig)
  application.set('postgres', postgres)

  return application
}

That will generate the same error but I can't remove the async from the handler.
And I can't re-open the issue.

Okay so the problem is that Node.js 8.10 now allows you to resolve using Promises instead of the old callback and even older context.succeed (which this library uses). We did have a path forward for upgrading from context.succeed to callback by setting context.callbackWaitsForEmptyEventLoop = true (see #32) however, the new Promise pattern ignores that.

For now, you can't use Node.js 8.10 with an async handler (or return a promise). I'll update the library to work with the latest updates soon. Based on the examples you've provided so far, it's recommended to perform these setup tasks outside of the handler for best performance (that way these things happen only on cold start, and not on every invocation)

You may be able to get this working in the current version if you return a promise from your handler and then patch context.succeed = resolve

I've tried what @brettstack said and it works (I'm using serverless-offline so I use process.env.IS_OFFLINE to pass correct handler to aws-serverless-express):

import awsServerlessExpress from 'aws-serverless-express';
import express from 'express';
import configureApp from './configureApp';

export default async (event, context) => {
  const app = express();

  await configureApp(app);

  app.get('/', function(req, res){
    res.send('hello world');
  });

  const server = awsServerlessExpress.createServer(app);

  return new Promise((resolve, reject) => {
    awsServerlessExpress.proxy(server, event, {
      ...context,
      succeed: process.env.IS_OFFLINE ? context.succeed : resolve,
    });
  });
}

@josephktcheung glad to hear it worked. Regarding your code, you should always try to move setup tasks to outside of the handler. You should see significant performance improvements if you do this, but take care that your code still functions correctly on cold start.

I'll give a try tomorrow. Thanks @josephktcheung to confirm that it worked.

Regarding moving stuff outside the handler, we usually do sth like that:

import awsServerlessExpress from 'aws-serverless-express';
import express from 'express';
import configureApp from './configureApp';

let server

export default async (event, context) => {
  if (server) {
    return awsServerlessExpress.proxy(server, event, context)
  }

  const app = express();

  await configureApp(app);

  app.get('/', function(req, res){
    res.send('hello world');
  });

  server = awsServerlessExpress.createServer(app)

  return awsServerlessExpress.proxy(server, event, context)
}

Otherwise I don't know how can you move await configureApp(app); outside the handler since it's an async call.

@j0k3r this is how I would now initialize the app outside of the handler using your code, I'm sure there are another ways to do it:

  1. Create an eventEmitter
  2. Initialize app and server outside of the handler
  3. Emit initialized event once initialization is done
  4. In handler check if server / app singleton is undefined, if it is undefined, wait for the initialized event
  5. return the promise that proxy event to aws-serverless-express server instance in the handler

(settimeout is to demonstrate the difference between cold and warm start, take it out for production)

import EventEmitter from 'events';
import awsServerlessExpress from 'aws-serverless-express';
import express from 'express';
import configureApp from './configureApp';

const app = express();
const server = awsServerlessExpress.createServer(app);

class AppEmitter extends EventEmitter {}
const emitter = new AppEmitter();

let appInstance;

const timeout = ms =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve();
    }, ms);
  });

timeout(3000)
  .then(() => configureApp(app))
  .then(configuredApp => {
    appInstance = configuredApp;
    emitter.emit('initialized');
  });

const onInitialized = () =>
  new Promise((resolve, reject) => {
    emitter.on('initialized', () => {
      console.log('initialized app');
      resolve();
    });
  });

const proxyEventToServer = (event, context) =>
  new Promise(resolve => {
    awsServerlessExpress.proxy(server, event, {
      ...context,
      succeed: process.env.IS_OFFLINE ? context.succeed : resolve,
    });
  });

export default async (event, context) => {
  if (!appInstance) {
    await onInitialized();
  } else {
    console.log('appInstance initialized already');
  }

  return proxyEventToServer(event, context);
};

@j0k3r that works fine since server exists outside of the handler. 馃憤

To support async handlers natively we'll need to bump major version since this will be a breaking change.

@brettstack no problem about bumping 馃憤
@josephktcheung that's an interesting way to handle initialization, thanks for sharing!

@brettstack Any progress about async support?

I have it working in a local branch. Going to go with a non-breaking change and we'll improve the overall API of this project in the future in a major version bump. I first want to add some additional integration tests before I merge in support. SAM has been taking priority for me lately, but I do want to get this in. For now I suggest patching in support https://github.com/awslabs/aws-serverless-express/issues/134#issuecomment-379417431

PR for this here https://github.com/awslabs/aws-serverless-express/pull/173. Note that I plan on improving the interface in a future breaking change (aws-serverless-express 4.0.0), but for now this is a backwards compatible change with the following interface:

exports.handler = async (event, context) => awsServerlessExpress.proxy(server, event, context, 'PROMISE').promise

Released in v3.3.3

I switched to what you did (and upgraded to 3.3.5):

-    return new Promise((resolve) => {
-      awsServerlessExpress.proxy(server, event, {
-        ...context,
-        succeed: resolve,
-      })
-    })
+    return awsServerlessExpress.proxy(server, event, context, 'PROMISE')

But now the APIGW returns 502 all the time. No error in log 馃槙

@j0k3r @Bnaya

Sorry, I goofed the guidance in that comment (now updated). Tack a .promise at the end of that line and you should be good to go.

Here's the test which covers this https://github.com/awslabs/aws-serverless-express/blob/master/__tests__/integration.js#L196-L200

And here's the implementation https://github.com/awslabs/aws-serverless-express/blob/master/src/index.js#L210-L230

You can see I'm returning { promise: new Promise... instead of returning new Promise... directly. This decision was made for future extensibility.

Oh great!
Yeah I'm aware of the .promise() in the AWS SDK already.
Sorry to bother you I didn't took the time to check the code.
Thanks!

Should this be working right now? I am using "SAM CLI, version 0.6.0" and throwing an exception in an async Lambda handler with node 8.10 seems to return status code 200 for me.

I haven't tried with SAM CLI, but it works in Lambda if you are using the latest version of this package. I'll try to get some updated docs and add an init/quickstart over at SAM CLI.

What do you mean by works in "Lambda"? As in the real Lambda on AWS? I noticed after responding here that this is not the repo for aws-sam-cli.

Should I be looking at some other issue for following the progress on this?

I also found this for example: https://github.com/awslabs/aws-sam-cli/issues/522 which might be what I am experiencing.

Sounds like that may be the Issue you're looking for. If you find it doesn't work in Lamdba (correct, I mean the one in AWS) please ping me again.

I don't currently have an integ test for application errors in Promise resolution mode (though I have one for the default callback mode https://github.com/awslabs/aws-serverless-express/blob/master/__tests__/integration.js#L413). I need to re-evaluate how I write these tests now that there exists multiple resolution modes. Copy/pasting the same test for different resolution modes isn't scalable like I have done with the happy path tests https://github.com/awslabs/aws-serverless-express/blob/master/__tests__/integration.js#L184

Ok, thanks. So I guess my issue is similar however different. And yes, it does work this way in the real Lambda.

I get unusual mysql connection timeout when I use async in hander for AWS Lambda. I think AWS Lambda handler does not like async (or is not compatible). Hence, preferred not to use async in handler module.exports function.

Does not work:
module.exports.sampleXYZ = async (event, context, callback) => { await synchronousFunction(); // Do something and return };

Works:
module.exports.sampleXYZ = (event, context, callback) => { call wrapperFunction(); // Call wrapper function and do synchronous stuff over there };

Thanks.

Same as @smrgrg here. I get logs inside my async function but then nothing than the "internal server" error as a return.

Comparing with @j0k3r sample code, I'm not able to go after this line

await configureApp(app);

Conf: AWS lambda with node.js 8 && aws-serverless-express v3.3.5

Does anybody solved it ?

@blueskycorner After I removed my async in handler export function, I had to update my webpack. After that, it works like a charm. I had to manage my eslint configurations though.

Sorry for the last post. The problem was on my side. I'm still new in node js dev ... I missed to specify a callback in my async function (configureApp).
@smrgrg: No need for me to remove the async in the lambda. The example provided by @j0k3r works perfectly.

So, what is the relevant solution ? 3.3.6
tried everything from this discussion and still can't get async handler working (

module.exports = awsServerlessExpress.proxy(server, event, context, 'PROMISE').promise should work

Yes, it works
Thx )

Hey, thanks for providing the solution here !

Having this:
https://github.com/awslabs/aws-serverless-express/issues/134#issuecomment-495026574

In the Readme or a sample would help big time for future users.

Thanks. We're working towards v4 where this will be the default.

I am a little bit unclear on the solution here. Is it possible to resolve this issue if you are not running a server? I am just running serverless offline straight in the command line and the exposed handlers are all async. Thank you for the help.

With @brettstack no longer at AWS it seems development here has stalled. Will plans for V4 be resurrected?

I'm now supporting this project again. V4 RCs are available http://npmjs.com/package/@vendia/serverless-express

Was this page helpful?
0 / 5 - 0 ratings