Skip to content

Unhandled Promise rejection when using ctx.res.send() in async afterRemote hook #416

Description

@henkosch

Description/Steps to reproduce

Look at the following example model:

module.exports = function (Test) {
    Test.solve = async function () {
        return 42;
    };

    Test.afterRemote('solve', async function (ctx, output) {
        ctx.res.setHeader('Content-Type', 'text/plain');
        ctx.res.send("Result: " + ctx.result);
    });

    Test.remoteMethod("solve", {
        http: { path: '/solve', verb: "get" },
        returns: { type: "number", root: true }
    });
};

I wanted to generate a plain text response for an endpoint. I could not find any other solution than to use an afterRemote hook to change the response. So I used ctx.res.send() to set the response explicitly inside the hook.

I'm using ES6 async functions everywhere where loopback supports it. These functions return promises so I can use await anywhere inside them which is very handy. So returning a promise can be used instead of calling next().

In the above example I get the following warning:

UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: Can't set headers after they are sent.

I tried to find the cause of this behavior, so I converted the afterRemote hook back to the callback version like this:

Test.afterRemote('solve', function (ctx, output, next) {
    ctx.res.setHeader('Content-Type', 'text/plain');
    ctx.res.send("Result: " + ctx.result);
    next();
});

But then I get the following warning:

UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: Callback was already called.

So I went further and removed any promises, and reverted my code back to the callback style:

Test.solve = function (next) {
    next(null, 42);
};

Test.afterRemote('solve', function (ctx, output, next) {
    ctx.res.setHeader('Content-Type', 'text/plain');
    ctx.res.send("Result: " + ctx.result);
    next();
});

Test.remoteMethod("solve", {
    http: { path: '/solve', verb: "get" },
    returns: { type: "number", root: true }
});

This crashed the nodejs process with the following error:

D:\dev\lbtest\node_modules\async\dist\async.js:903
        if (fn === null) throw new Error("Callback was already called.");
                         ^

Error: Callback was already called.
    at D:\dev\lbtest\node_modules\async\dist\async.js:903:32
    at D:\dev\lbtest\node_modules\async\dist\async.js:3858:13
    at D:\dev\lbtest\node_modules\strong-remoting\lib\remote-objects.js:717:7
    at execStack (D:\dev\lbtest\node_modules\strong-remoting\lib\remote-objects.js:522:7)
    at RemoteObjects.execHooks (D:\dev\lbtest\node_modules\strong-remoting\lib\remote-objects.js:526:10)
    at interceptInvocationErrors (D:\dev\lbtest\node_modules\strong-remoting\lib\remote-objects.js:716:10)
    at D:\dev\lbtest\node_modules\strong-remoting\node_modules\loopback-phase\node_modules\async\lib\async.js:148:21
    at D:\dev\lbtest\node_modules\strong-remoting\node_modules\loopback-phase\node_modules\async\lib\async.js:148:21
    at D:\dev\lbtest\node_modules\strong-remoting\node_modules\loopback-phase\node_modules\async\lib\async.js:148:21
    at D:\dev\lbtest\node_modules\strong-remoting\lib\remote-objects.js:679:7
    at D:\dev\lbtest\node_modules\strong-remoting\lib\http-context.js:303:16
    at SharedMethod.invoke (D:\dev\lbtest\node_modules\strong-remoting\lib\shared-method.js:286:12)
    at HttpContext.invoke (D:\dev\lbtest\node_modules\strong-remoting\lib\http-context.js:297:12)
    at phaseInvoke (D:\dev\lbtest\node_modules\strong-remoting\lib\remote-objects.js:677:9)
    at runHandler (D:\dev\lbtest\node_modules\strong-remoting\node_modules\loopback-phase\lib\phase.js:135:5)
    at iterate (D:\dev\lbtest\node_modules\strong-remoting\node_modules\loopback-phase\node_modules\async\lib\async.js:146:13)

So it seems the ctx.res.send() somehow calls the next() callback and closes the middleware pipeline. But this behavior interferes with the promise that is returned by the afterRemote hook, so it will be rejected, but the rejection is not handled.

Expected result

The example at the top should work, and should not produce an unhandled promise rejection warning, because it is deprecated and will crash the nodejs process in the future.

And since it's supported to return a promise inside an afterRemote hook, it should not be a problem to use ctx.res.send() inside an async hook.

Additional information

I am also interested in better ways to send text/plain response with a remoteMethod without an afterRemote hook.

Metadata

Metadata

Assignees

Labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions