Handled invalid timestamp format in filters

fix https://linear.app/tryghost/issue/SLO-85/fix-http-500-on-contentposts

- in the event we give the incorrect format in a filter, MySQL will
  throw an error and we'll throw a HTTP 500 error
- we can capture this error and return a more useful error to the user
- ideally we'd do this in a validation step before attempting the query,
  but parsing this out of NQL and detecting which columns are DATETIME
  could be quite tricky
This commit is contained in:
Daniel Lockyer 2024-05-07 17:42:14 +02:00 committed by Daniel Lockyer
parent 82c612bad9
commit ae88dc8548
4 changed files with 76 additions and 10 deletions

View File

@ -4909,3 +4909,21 @@ Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac tu
],
}
`;
exports[`Posts Content API Errors upon invalid filter value 1: [body] 1`] = `
Object {
"errors": Array [
Object {
"code": "ER_WRONG_VALUE",
"context": "Invalid value select count(distinct posts.id) as aggregate from \`posts\` where (\`posts\`.\`status\` = 'published' and (\`posts\`.\`published_at\` < '1715091791890' and \`posts\`.\`type\` = 'post')) - Incorrect DATETIME value: '1715091791890'",
"details": null,
"ghostErrorCode": null,
"help": null,
"id": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
"message": "Validation error, cannot list posts.",
"property": null,
"type": "ValidationError",
},
],
}
`;

View File

@ -5,7 +5,7 @@ const testUtils = require('../../utils');
const models = require('../../../core/server/models');
const {agentProvider, fixtureManager, matchers, mockManager} = require('../../utils/e2e-framework');
const {anyArray, anyContentVersion, anyEtag, anyUuid, anyISODateTimeWithTZ} = matchers;
const {anyArray, anyContentVersion, anyErrorId, anyEtag, anyUuid, anyISODateTimeWithTZ} = matchers;
const postMatcher = {
published_at: anyISODateTimeWithTZ,
@ -93,6 +93,21 @@ describe('Posts Content API', function () {
});
});
it('Errors upon invalid filter value', async function () {
if (process.env.NODE_ENV !== 'testing-mysql') {
this.skip();
}
await agent
.get(`posts/?filter=published_at%3A%3C%271715091791890%27`)
.expectStatus(422)
.matchBodySnapshot({
errors: [{
id: anyErrorId
}]
});
});
it('Can filter posts by tag', async function () {
const res = await agent.get('posts/?filter=tag:kitchen-sink,featured:true&include=tags')
.expectStatus(200)

View File

@ -9,6 +9,7 @@ const tpl = require('@tryghost/tpl');
const messages = {
genericError: 'An unexpected error occurred, please try again.',
invalidValue: 'Invalid value',
pageNotFound: 'Page not found',
resourceNotFound: 'Resource not found',
methodNotAcceptableVersionAhead: {
@ -92,12 +93,21 @@ module.exports.prepareError = function prepareError(err, req, res, next) {
} else if (isDependencyInStack('mysql2', err)) {
// we don't want to return raw database errors to our users
err.sqlErrorCode = err.code;
err = new errors.InternalServerError({
err: err,
message: tpl(messages.genericError),
statusCode: err.statusCode,
code: 'UNEXPECTED_ERROR'
});
if (err.code === 'ER_WRONG_VALUE') {
err = new errors.ValidationError({
message: tpl(messages.invalidValue),
context: err.message,
err
});
} else {
err = new errors.InternalServerError({
err: err,
message: tpl(messages.genericError),
statusCode: err.statusCode,
code: 'UNEXPECTED_ERROR'
});
}
// For everything else, create a generic 500 error, with context set to the original error message
} else {
err = new errors.InternalServerError({

View File

@ -103,7 +103,7 @@ describe('Prepare Error', function () {
});
});
it('Correctly prepares a mysql2 error', function (done) {
it('Correctly prepares a known ER_WRONG_VALUE mysql2 error', function (done) {
let error = new Error('select anything from anywhere where something = anything;');
error.stack += '\n';
@ -112,6 +112,29 @@ describe('Prepare Error', function () {
error.sql = 'select anything from anywhere where something = anything;';
error.sqlMessage = 'Incorrect DATETIME value: 3234234234';
prepareError(error, {}, {
set: () => {}
}, (err) => {
assert.equal(err.statusCode, 422);
assert.equal(err.name, 'ValidationError');
assert.equal(err.message, 'Invalid value');
assert.equal(err.code, 'ER_WRONG_VALUE');
assert.equal(err.sqlErrorCode, 'ER_WRONG_VALUE');
assert.equal(err.sql, 'select anything from anywhere where something = anything;');
assert.equal(err.sqlMessage, 'Incorrect DATETIME value: 3234234234');
done();
});
});
it('Correctly prepares an unknown mysql2 error', function (done) {
let error = new Error('select anything from anywhere where something = anything;');
error.stack += '\n';
error.stack += path.join('node_modules', 'mysql2', 'lib');
error.code = 'ER_BAD_FIELD_ERROR';
error.sql = 'select anything from anywhere where something = anything;';
error.sqlMessage = 'Incorrect value: erororoor';
prepareError(error, {}, {
set: () => {}
}, (err) => {
@ -119,9 +142,9 @@ describe('Prepare Error', function () {
assert.equal(err.name, 'InternalServerError');
assert.equal(err.message, 'An unexpected error occurred, please try again.');
assert.equal(err.code, 'UNEXPECTED_ERROR');
assert.equal(err.sqlErrorCode, 'ER_WRONG_VALUE');
assert.equal(err.sqlErrorCode, 'ER_BAD_FIELD_ERROR');
assert.equal(err.sql, 'select anything from anywhere where something = anything;');
assert.equal(err.sqlMessage, 'Incorrect DATETIME value: 3234234234');
assert.equal(err.sqlMessage, 'Incorrect value: erororoor');
done();
});
});