Silent validation error with Sequelize

In my sequelize model, I have a unique constraint (reflects the UNIQUE constraint in Postgres).
Problem is, when in ForestAdmin I perform an illegal operation (I update a record and violates the UNIQUE constraint on a column), the back crashes and the action in ForestAdmin times out.

I would have expected ForestAdmin to show me the validation error message?
If this silent error is by design, why is that the case, and how can I work around it?

thanks

Hi @francois_Ruty

Could you share:

  • the way your unique constraint is defined in your Sequelize model
  • the logs of the crash if any
  • the version of the forest-express-sequelize agent you are currently using (that is one important question in the default post template)

Thanks for your help.

Hello, sorry,

here is the constraint in models file:

immatriculation: {
  type: DataTypes.STRING,
  unique: true
},

Logs of the crash:

PUT /forest/gsheetClients/10206/relationships/vehicule_immat 204 - - 56.040 ms
Unhandled rejection SequelizeUniqueConstraintError: Validation error
at Query.formatError (/usr/src/app/node_modules/sequelize/lib/dialects/postgres/query.js:312:18)
at query.catch.err (/usr/src/app/node_modules/sequelize/lib/dialects/postgres/query.js:71:18)
at tryCatcher (/usr/src/app/node_modules/sequelize/node_modules/bluebird/js/release/util.js:16:23)
at Promise._settlePromiseFromHandler (/usr/src/app/node_modules/sequelize/node_modules/bluebird/js/release/promise.js:547:31)
at Promise._settlePromise (/usr/src/app/node_modules/sequelize/node_modules/bluebird/js/release/promise.js:604:18)
at Promise._settlePromise0 (/usr/src/app/node_modules/sequelize/node_modules/bluebird/js/release/promise.js:649:10)
at Promise._settlePromises (/usr/src/app/node_modules/sequelize/node_modules/bluebird/js/release/promise.js:725:18)
at _drainQueueStep (/usr/src/app/node_modules/sequelize/node_modules/bluebird/js/release/async.js:93:12)
at _drainQueue (/usr/src/app/node_modules/sequelize/node_modules/bluebird/js/release/async.js:86:9)
at Async._drainQueues (/usr/src/app/node_modules/sequelize/node_modules/bluebird/js/release/async.js:102:5)
at Immediate.Async.drainQueues [as _onImmediate] (/usr/src/app/node_modules/sequelize/node_modules/bluebird/js/release/async.js:15:14)
at runCallback (timers.js:705:18)
at tryOnImmediate (timers.js:676:5)
at processImmediate (timers.js:658:5)
at process.topLevelDomainCallback (domain.js:126:23)

forest-express-sequelize version in package.json:
^5.2.0

Hi @francois_Ruty, I can’t reproduce your error, seems like I don’t even need to add it to my model to get a popup with the error message.

ok then we need to figure out the difference between my setup and yours

what is FA mechanism to display backend errors, which key in json payload is it watching for?

It is done via sequelize, when it tries to commit your data into your database. Do you have the constraint set in your database ?

Yes, the UNIQUE constraint exists in DB (postgres).

I just recalled I override the PUT route for this model, here is the code:

// Update a Gsheet Client
router.put('/gsheetClients/:recordId', permissionMiddlewareCreator.update(), (request, response, next) => {
  // Learn what this route does here: https://docs.forestadmin.com/documentation/v/v5/reference-guide/routes/default-routes#update-a-record

  const recordUpdater = new RecordUpdater(gsheetClients);
  recordUpdater.deserialize(request.body)
    .then(recordToUpdate => {
      return recordUpdater.update(recordToUpdate, request.params.recordId)
    })
    .then(record => {
      return recordUpdater.serialize(record)
    })
    .then(recordSerialized => {
      const vehicule_id = request.body.data.relationships.vehicule_immat.data.id;
      vehicules.findOne({
        where: {
          id: vehicule_id
        }
      }).then(data => {
        const immat = data.dataValues.immat;
        gsheetClients.update(
          { immatriculation: immat }, { where: { id: request.params.recordId }}
        ).then(data => {
          //recordSerialized.data.relationships.vehicule_immat.data = immat;
          console.log(recordSerialized.data.relationships.vehicule_immat);
          response.send(recordSerialized);
        })
      });
    })
    .catch(next);

  

  
});

I just used the documentation and copy/pasted the code, and made some modification (essentially to store a linked record column other than id). I assume a .catch is missing after the gsheetClients.update, what is the syntax the .catch response must respect, for FA to process the error message?
It would be great to have this information in https://docs.forestadmin.com/documentation/v/v5/reference-guide/routes/default-routes#update-a-record

Hi @francois_Ruty, I tried to reproduce with the same route override as yours but the error toaster still shows up.

To try to understand what goes wrong could you please share with me the HTTP PUT call made when editing the record (with the server answer), maybe a screen share of the issue with the network console open might help (using loom for example)

Also, does this happen only when editing a record or is the error silent too when creating a record?

Hello, when I create a record, here are the headers + payload:

POST /forest/gsheetClients HTTP/1.1
Host: localhost:3310
Connection: keep-alive
Content-Length: 186
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjMxODQwIiwiZW1haWwiOiJwYXNjYWwuZ2hvc29uQGZpbmRyaXZlLmNvbSIsImZpcnN0TmFtZSI6IlBhc2NhbCIsImxhc3ROYW1lIjoiR2hvc29uIiwidGVhbSI6Ik9wZXJhdGlvbnMiLCJyZW5kZXJpbmdJZCI6IjU3Mjk4IiwiaWF0IjoxNTk3MDgxOTAyLCJleHAiOjE1OTgyOTE1MDJ9.0TniYtjk7zuNF9UIkqJcokiNmMJvxtoi4cm-F4CyXmw
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36
Content-Type: application/json
Origin: http://app.forestadmin.com
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://app.forestadmin.com/Findrive/Development/Operations/data/76bd3a30-9526-11ea-9f36-4f4d1a1e01d9/index/record/76bd3a30-9526-11ea-9f36-4f4d1a1e01d9/create/details
Accept-Encoding: gzip, deflate, br
Accept-Language: fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7

{“data”:{“attributes”:{“apport”:1,“assurance”:“t”,“siren”:“tttt”,“societe”:“tttttt”},“relationships”:{“vehicule_immat”:{“data”:{“type”:“vehicules”,“id”:“6020”}}},“type”:“gsheetClients”}}

This gives:


The toaster appears but with some garbage.

Server error:

POST /forest/gsheetClients 500 1802 - 25.055 ms
SequelizeUniqueConstraintError: Validation error
at Query.formatError (/usr/src/app/node_modules/sequelize/lib/dialects/postgres/query.js:312:18)
at query.catch.err (/usr/src/app/node_modules/sequelize/lib/dialects/postgres/query.js:71:18)
at tryCatcher (/usr/src/app/node_modules/sequelize/node_modules/bluebird/js/release/util.js:16:23)
at Promise._settlePromiseFromHandler (/usr/src/app/node_modules/sequelize/node_modules/bluebird/js/release/promise.js:547:31)
at Promise._settlePromise (/usr/src/app/node_modules/sequelize/node_modules/bluebird/js/release/promise.js:604:18)
at Promise._settlePromise0 (/usr/src/app/node_modules/sequelize/node_modules/bluebird/js/release/promise.js:649:10)
at Promise._settlePromises (/usr/src/app/node_modules/sequelize/node_modules/bluebird/js/release/promise.js:725:18)
at _drainQueueStep (/usr/src/app/node_modules/sequelize/node_modules/bluebird/js/release/async.js:93:12)
at _drainQueue (/usr/src/app/node_modules/sequelize/node_modules/bluebird/js/release/async.js:86:9)
at Async._drainQueues (/usr/src/app/node_modules/sequelize/node_modules/bluebird/js/release/async.js:102:5)
at Immediate.Async.drainQueues [as _onImmediate] (/usr/src/app/node_modules/sequelize/node_modules/bluebird/js/release/async.js:15:14)
at runCallback (timers.js:705:18)
at tryOnImmediate (timers.js:676:5)
at processImmediate (timers.js:658:5)
at process.topLevelDomainCallback (domain.js:126:23)

If I update a record, here are headers + payload:

Headers:

PUT /forest/gsheetClients/10210 HTTP/1.1
Host: localhost:3310
Connection: keep-alive
Content-Length: 162
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjMxODQwIiwiZW1haWwiOiJwYXNjYWwuZ2hvc29uQGZpbmRyaXZlLmNvbSIsImZpcnN0TmFtZSI6IlBhc2NhbCIsImxhc3ROYW1lIjoiR2hvc29uIiwidGVhbSI6Ik9wZXJhdGlvbnMiLCJyZW5kZXJpbmdJZCI6IjU3Mjk4IiwiaWF0IjoxNTk3MDgxOTAyLCJleHAiOjE1OTgyOTE1MDJ9.0TniYtjk7zuNF9UIkqJcokiNmMJvxtoi4cm-F4CyXmw
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36
Content-Type: application/json
Origin: http://app.forestadmin.com
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://app.forestadmin.com/Findrive/Development/Operations/data/76bd3a30-9526-11ea-9f36-4f4d1a1e01d9/index/record/76bd3a30-9526-11ea-9f36-4f4d1a1e01d9/10210/details
Accept-Encoding: gzip, deflate, br
Accept-Language: fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7

Payload:
{“data”:{“id”:“10210”,“attributes”:{},“relationships”:{“client”:{“data”:null},“vehicule_immat”:{“data”:{“type”:“vehicules”,“id”:“6020”}}},“type”:“gsheetClients”}}

Result in FA:


No toaster, just an infinite spinner

Server error:

PUT /forest/gsheetClients/10210 - - - - ms
Unhandled rejection SequelizeUniqueConstraintError: Validation error
at Query.formatError (/usr/src/app/node_modules/sequelize/lib/dialects/postgres/query.js:312:18)
at query.catch.err (/usr/src/app/node_modules/sequelize/lib/dialects/postgres/query.js:71:18)
at tryCatcher (/usr/src/app/node_modules/sequelize/node_modules/bluebird/js/release/util.js:16:23)
at Promise._settlePromiseFromHandler (/usr/src/app/node_modules/sequelize/node_modules/bluebird/js/release/promise.js:547:31)
at Promise._settlePromise (/usr/src/app/node_modules/sequelize/node_modules/bluebird/js/release/promise.js:604:18)
at Promise._settlePromise0 (/usr/src/app/node_modules/sequelize/node_modules/bluebird/js/release/promise.js:649:10)
at Promise._settlePromises (/usr/src/app/node_modules/sequelize/node_modules/bluebird/js/release/promise.js:725:18)
at _drainQueueStep (/usr/src/app/node_modules/sequelize/node_modules/bluebird/js/release/async.js:93:12)
at _drainQueue (/usr/src/app/node_modules/sequelize/node_modules/bluebird/js/release/async.js:86:9)
at Async._drainQueues (/usr/src/app/node_modules/sequelize/node_modules/bluebird/js/release/async.js:102:5)
at Immediate.Async.drainQueues [as _onImmediate] (/usr/src/app/node_modules/sequelize/node_modules/bluebird/js/release/async.js:15:14)
at runCallback (timers.js:705:18)
at tryOnImmediate (timers.js:676:5)
at processImmediate (timers.js:658:5)
at process.topLevelDomainCallback (domain.js:126:23)

So in the case of a POST, the problem is that the toaster is garbage, and in the PUT, there is no toaster at all.

Both POST and PUT routes are overridden, here is the source code:

// Create a Gsheet Client
router.post('/gsheetClients', permissionMiddlewareCreator.create(), (request, response, next) => {
  // Learn what this route does here: https://docs.forestadmin.com/documentation/v/v5/reference-guide/routes/default-routes#create-a-record

  const recordCreator = new RecordCreator(gsheetClients);
  recordCreator.deserialize(request.body)
    .then(recordToCreate => recordCreator.create(recordToCreate))
    .then(record => recordCreator.serialize(record))
    .then(recordSerialized => {
      const vehicule_id = request.body.data.relationships.vehicule_immat.data.id;
      vehicules.findOne({
        where: {
          id: vehicule_id
        }
      }).then(data => {
        const immat = data.dataValues.immat;
        gsheetClients.update(
          { immatriculation: immat }, { where: { id: recordSerialized.data.id }}
        ).then(data => {
          response.send(recordSerialized);
        })
      })
    })
    .catch(next);

});

// Update a Gsheet Client
router.put('/gsheetClients/:recordId', permissionMiddlewareCreator.update(), (request, response, next) => {
  // Learn what this route does here: https://docs.forestadmin.com/documentation/v/v5/reference-guide/routes/default-routes#update-a-record

  const recordUpdater = new RecordUpdater(gsheetClients);
  recordUpdater.deserialize(request.body)
    .then(recordToUpdate => {
      return recordUpdater.update(recordToUpdate, request.params.recordId)
    })
    .then(record => {
      return recordUpdater.serialize(record)
    })
    .then(recordSerialized => {
      const vehicule_id = request.body.data.relationships.vehicule_immat.data.id;
      vehicules.findOne({
        where: {
          id: vehicule_id
        }
      }).then(data => {
        const immat = data.dataValues.immat;
        gsheetClients.update(
          { immatriculation: immat }, { where: { id: request.params.recordId }}
        ).then(data => {
          //recordSerialized.data.relationships.vehicule_immat.data = immat;
          console.log(recordSerialized.data.relationships.vehicule_immat);
          response.send(recordSerialized);
        })
      });
    })
    .catch(next);

  

  
});

Thank you for all those info.

I’ve been able to reproduce and acknowledge the badly formatted error message on the post request. We’ll work on a fix for that.

However, I can’t understand why the PUT call does not return a response. I’ve only been able to reproduce the infinite spin by removing the .catch(next).

Could you ensure that both the POST and PUT requests are going through the .catch(next)?

For example by replacing it by:

.catch((error) => {
  console.log('THERE')
  return next(error)
});

Thanks.

Hello, I performed the replacement in both PUT and POST.

POST is going through the catch, but not the PUT

Hey @francois_Ruty,

Just a guess but could you try to return the update like so

// Create a Gsheet Client
router.post('/gsheetClients', permissionMiddlewareCreator.create(), (request, response, next) => {
  // Learn what this route does here: https://docs.forestadmin.com/documentation/v/v5/reference-guide/routes/default-routes#create-a-record

  const recordCreator = new RecordCreator(gsheetClients);
  recordCreator.deserialize(request.body)
    .then(recordToCreate => recordCreator.create(recordToCreate))
    .then(record => recordCreator.serialize(record))
    .then(recordSerialized => {
      const vehicule_id = request.body.data.relationships.vehicule_immat.data.id;
      vehicules.findOne({
        where: {
          id: vehicule_id
        }
      }).then(data => {
        const immat = data.dataValues.immat;
        return gsheetClients.update(
          { immatriculation: immat }, { where: { id: recordSerialized.data.id }}
        ).then(data => {
          response.send(recordSerialized);
        })
      })
    })
    .catch(next);

});

// Update a Gsheet Client
router.put('/gsheetClients/:recordId', permissionMiddlewareCreator.update(), (request, response, next) => {
  // Learn what this route does here: https://docs.forestadmin.com/documentation/v/v5/reference-guide/routes/default-routes#update-a-record

  const recordUpdater = new RecordUpdater(gsheetClients);
  recordUpdater.deserialize(request.body)
    .then(recordToUpdate => {
      return recordUpdater.update(recordToUpdate, request.params.recordId)
    })
    .then(record => {
      return recordUpdater.serialize(record)
    })
    .then(recordSerialized => {
      const vehicule_id = request.body.data.relationships.vehicule_immat.data.id;
      vehicules.findOne({
        where: {
          id: vehicule_id
        }
      }).then(data => {
        const immat = data.dataValues.immat;
        return gsheetClients.update(
          { immatriculation: immat }, { where: { id: request.params.recordId }}
        ).then(data => {
          //recordSerialized.data.relationships.vehicule_immat.data = immat;
          console.log(recordSerialized.data.relationships.vehicule_immat);
          response.send(recordSerialized);
        })
      });
    })
    .catch(next);
});

Hello, it doesn’t work, however my first instinct was right, a .catch is missing.
Now with this code:

// Update a Gsheet Client
router.put('/gsheetClients/:recordId', permissionMiddlewareCreator.update(), (request, response, next) => {
  // Learn what this route does here: https://docs.forestadmin.com/documentation/v/v5/reference-guide/routes/default-routes#update-a-record

  const recordUpdater = new RecordUpdater(gsheetClients);
  recordUpdater.deserialize(request.body)
    .then(recordToUpdate => {
      return recordUpdater.update(recordToUpdate, request.params.recordId)
    })
    .then(record => {
      return recordUpdater.serialize(record)
    })
    .then(recordSerialized => {
      const vehicule_id = request.body.data.relationships.vehicule_immat.data.id;
      vehicules.findOne({
        where: {
          id: vehicule_id
        }
      }).then(data => {
        const immat = data.dataValues.immat;
        gsheetClients.update(
          { immatriculation: immat }, { where: { id: request.params.recordId }}
        ).then(data => {
          //recordSerialized.data.relationships.vehicule_immat.data = immat;
          console.log(recordSerialized.data.relationships.vehicule_immat);
          response.send(recordSerialized);
        }).catch(next);
      });
    })
    .catch(next);

  

  
});

The outer action has a catch, but the outer does not fail, it’s the inner action that fails, so it needs its .catch as well

I get a toaster in FA with the same garbage error than with POST route.

Do you confirm that you recorded the toaster error message bug? (message being html garbage with no detail about the error)

2 Likes

Hi @francois_Ruty :wave:

I get a toaster in FA with the same garbage error than with POST route.
Do you confirm that you recorded the toaster error message bug? (message being html garbage with no detail about the error)

I’ve just created an issue on our side about this problem: we have a clear scenario about how to reproduce the issue thanks to your feedback! :+1: We will keep you posted as soon as we fixed it! Thank you for your patience :pray:

1 Like