question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Multiple async actions in response to a request

See original GitHub issue

I’m new to Koa (and backend-dev in general) and I’m trying to set up a simple signup api. When a user posts to /api/user, bcrypt hashes the password and the user information is stored in a database. In my code it successfully does this but Node returns “headers have already been sent” error and returns a 404 response to the client. I have no trouble doing either of these async actions on their own but when I try to figure out a way to do them one after the other the headers always get sent early.

Here is my code:

async function createUser (ctx, req, next) {
  try {
    let d = new Date()
    await db.none('INSERT INTO users(email, username, hash, created, permissions) VALUES($1, $2, $3, $4, $5)', [req.email, req.username, req.pass, d, 'user'])
    ctx.status = 200
    next()
  } catch (e) {
    if (e.constraint === 'users_email_uindex') {
      ctx.response.body = 'This email address is already in use'
    } else if (e.constraint === 'users_username_uindex') {
      ctx.response.body = 'This username is already in use'
    }
    ctx.status = 400
    console.log(e)
    next()
  }
}

userService.post('/', async (ctx, next) => {
  console.log('POST /api/user/')
  let req = ctx.request.body
  try {
    await bcrypt.hash(req.pass, 10, (e, hash) => {
      if (e) {
        console.log(e)
      }
      req.pass = hash
      createUser(ctx, req, next)
    })
  } catch (e) {
    console.log(e)
  }
})

Issue Analytics

  • State:closed
  • Created 6 years ago
  • Comments:6 (2 by maintainers)

github_iconTop GitHub Comments

1reaction
uniibucommented, Jul 16, 2017

Using your code i would do it like this:

Changes:(Node v8.x+)

  • Use of ctx.body instead of ctx.response.body
  • Added ctx.body on createUser successful db insert, instead of just ctx.status 200
  • Removed next on createUser
  • Uses Node v8+ for util.promisify
  • Added constant bchash which is a promisified version of bcrypt.hash
const bcrypt = require("bcrypt")
const {promisify} = require('util')
const bchash = promisify(bcrypt.hash);

async function createUser(ctx, req) {
    try {
        let d = new Date();
        await db.none('INSERT INTO users(email, username, hash, created, permissions) VALUES($1, $2, $3, $4, $5)', [req.email, req.username, req.pass, d, 'user']);
        // simply setting ctx.status = 200 wont work, it will return as 204 No Content, so ctx.body must be set if you want a blank 200 response
        ctx.body = '';
    } catch (e) {
        console.log(e);
        ctx.status = 400;
        if (e.constraint === 'users_email_uindex') {
            ctx.body = 'This email address is already in use';
        } else if (e.constraint === 'users_username_uindex') {
            ctx.body = 'This username is already in use';
        }
    }
}

userService.post('/', async ctx => {
    console.log('POST /api/user/');
    let req = ctx.request.body;
    try {
        req.pass = await bchash(req.pass, 10);
        await createUser(ctx, req);
    } catch (e) {
        console.log(e);
    }
});

Changes:(Node < v8.x)

  • Pretty much the same as the first example except this does not use Node v8’s util promisify
  • Uses Node’s native Promise
async function createUser(ctx, req) {
    try {
        let d = new Date();
        await db.none('INSERT INTO users(email, username, hash, created, permissions) VALUES($1, $2, $3, $4, $5)', [req.email, req.username, req.pass, d, 'user']);
        ctx.body = '';
    } catch (e) {
        console.log(e);
        ctx.status = 400;
        if (e.constraint === 'users_email_uindex') {
            ctx.body = 'This email address is already in use';
        } else if (e.constraint === 'users_username_uindex') {
            ctx.body = 'This username is already in use';
        }
    }
}

async function bchash(pw, rounds) {
    return new Promise((resolve, reject) => {
        bcrypt.hash(pw, rounds, (err, hash) => {
            if (err)
                reject(err);
            else
                resolve(hash);
        });
    });
}

userService.post('/', async ctx => {
    console.log('POST /api/user/');
    let req = ctx.request.body;
    try {
        req.pass = await bchash(req.pass, 10);
        await createUser(ctx, req);
    } catch (e) {
        console.log(e);
    }
});

Changes:(My ow version, Node v8+)

  • Simplified the routes controller to a single async function
  • Uses ctx.throw for error handling
const bcrypt = require("bcrypt")
const {promisify} = require('util')
const bchash = promisify(bcrypt.hash);

async function createUser(ctx) {
    let req = ctx.request.body;
    let d = new Date();
    try {
        req.pass = await bchash(req.pass, 10);
        await db.none('INSERT INTO users(email, username, hash, created, permissions) VALUES($1, $2, $3, $4, $5)', [req.email, req.username, req.pass, d, 'user']);
        ctx.body = 'Success';
    } catch (e) {
        console.log(e);
        if (e.constraint === 'users_email_uindex') {
            ctx.throw('This email address is already in use', 400);
        } else if (e.constraint === 'users_username_uindex') {
            ctx.throw('This username is already in use', 400);
        }
    }
}
userService.post('/', createUser);

NOTE: I did not include whatever module you use for your router, but it should basically be the same with most router syntax.

0reactions
uniibucommented, Jul 16, 2017

Glad to be of help. My best guess is that you are using a lower version of bcrypt(<1.x) which doesn’t support promises. The latest version of bcrypt (1.0.2) supports promises. So in case you update your bcrypt module, you could use await directly like req.pass = await bcrypt.hash(req.pass, 10)

In regards with ctx.throw i just followed the one on docs https://github.com/koajs/koa/wiki/Error-Handling, which is probably outdated due to changes from http-errors

Cheers!

Read more comments on GitHub >

github_iconTop Results From Across the Web

Dispatch multiple async actions with Redux Toolkit
So my question now is: how do I dispatch this action multiple times in an "async/await" way so the catalogs are loaded one...
Read more >
How to dispatch multiple async actions in one function in ...
Is there some workaround? Yes. Let's create an array of all our async functions that need to be called. After that, our dispatch...
Read more >
Use Promise.all to Stop Async/Await from Blocking ...
Promises are extremely powerful for handling asynchronous operations in ... Fetch sends off a request to the Dog CEO REST API and waits...
Read more >
createAsyncThunk
createAsyncThunk. Overview​. A function that accepts a Redux action type string and a callback function that should return a promise.
Read more >
Aggregate Multiple API Requests with Promise.all()
Asynchronous operations utilize promises, callback functions, or async/await. Commonly, these operations are singular and do not require ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found