Search code examples
node.jsmongodbmongoosekoakoa-router

Error: Can't set headers after they are sent using koa


Stack Overflow has a ton of questions regarding this error. Every one with its context. I have tried applying most of the solutions suggested but none is working. I am also new to koa and mongoose

"use strict"
const Koa = require('koa')
const Router = require('koa-router')
const router = new Router()
const mongoose = require('mongoose')
const Pug = require('koa-pug')
const app = new Koa()
const KoaBody = require('koa-body')
const koaBody = new KoaBody()
mongoose.Promise = global.Promise

mongoose.connect('mongodb://localhost/my_db', {useMongoClient: true, promiseLibrary: global.Promise})

const personSchema = mongoose.Schema({
   name: String,
   age: Number,
   nationality: String
})
const Person = mongoose.model("Person", personSchema)

const pug = new Pug({
  viewPath: '.',
})

router.get('/person', async ctx => {
  ctx.render('person')
})

router.post('/person', koaBody, async (ctx, next) => {
         const personInfo = ctx.request.body
         if (!personInfo.name || !personInfo.age || !personInfo.nationality)   {
          return ctx.render('show_message', {message: "Sorry, you provided wrong info", type: "error"})
     }
     else {
         const newPerson = new Person({
             name: personInfo.name,
             age: personInfo.age,
             nationality: personInfo.nationality
         })
         if (ctx.response.headerSent) console.log("sent 0")
         newPerson.save(function (err) {
             if (ctx.response.headerSent) console.log("sent 1")
             if (err) {
                 return err
             }
             else {
                 if (ctx.response.headerSent) console.log("sent 2")
                 ctx.response.flushHeaders()
                 ctx.render('show_message', {
                     message: "New person added", type: "success", person: personInfo
                 })
             }
         })
     }
 })

pug.use(app)
app.use(router.routes())
app.listen(3000, () => {
console.log("We are listening for connections on the server")
})

When I execute the code I get the following error:

We are listening for connections on the server
sent 1
sent 2
events.js:182
  throw er; // Unhandled 'error' event
  ^

Error: Can't set headers after they are sent.
at validateHeader (_http_outgoing.js:504:11)
at ServerResponse.setHeader (_http_outgoing.js:511:3)
at Object.set (C:\Users\nahas\OneDrive\Code\Javascript\Node Modules\node_modules\koa\lib\response.js:440:16)
at Object.set type [as type] (C:\Users\nahas\OneDrive\Code\Javascript\Node Modules\node_modules\koa\lib\response.js:310:12)
at Object.type (C:\Users\nahas\OneDrive\Code\Javascript\Node Modules\node_modules\delegates\index.js:92:31)
at Object.contextRenderer [as render] (C:\Users\nahas\OneDrive\Code\Javascript\Node Modules\node_modules\koa-pug\src\pug.js:107:15)
at C:\Users\nahas\OneDrive\Code\Javascript\Node Modules\learn-koa\app.js:49:25
at C:\Users\nahas\OneDrive\Code\Javascript\Node Modules\node_modules\mongoose\lib\model.js:3835:16
at C:\Users\nahas\OneDrive\Code\Javascript\Node Modules\node_modules\mongoose\lib\services\model\applyHooks.js:162:20
at _combinedTickCallback (internal/process/next_tick.js:95:7)
at process._tickCallback (internal/process/next_tick.js:161:9)

I tried to identify exactly where the headers are sent by logging on the console when they are sent. As you can see from the output of the error, the headers are sent immediately after this line:

newPerson.save(function (err) {

Which means the mongoose save function is sending headers to the user. How do go about this problem?


Solution

  • newPerson.save() is an asynchronous function. You pass a callback function to it which gets called when the save operation is finished. By the time it gets called however, your router function already returned and so the headers where already send.

    Thats where the async/await pattern comes into play. As you are already in an async function, all you need to do is await the result of .save(). You can await promises. Luckily mongoose .save() returns a promise if you are not passing a callback. That means you can just await it. so...

    await newPerson.save()
    ctx.render('show_message', {
        message: "New person added", type: "success", person: personInfo
    })
    

    error handling is done a little differently when using async await, or better error handling is done properly again ;-).

    You'll use try/catch for error handling as you normally would in js.

    try{
        await newPerson.save()
        ctx.render(...)
    }catch(err){
        ctx.status=500
        ctx.body="somthing went wrong"  
    }