mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-10 23:36:14 -05:00
Improve API error handling
close #2757, refs #5286 - moves error formatting from api/index into errors lib - moves error handling from api/index into its own middleware - adds extra middleware for method not allowed which captures all unsupported routes
This commit is contained in:
parent
b15f1daf5a
commit
254e0f0597
10 changed files with 141 additions and 69 deletions
|
@ -21,10 +21,8 @@ var _ = require('lodash'),
|
||||||
authentication = require('./authentication'),
|
authentication = require('./authentication'),
|
||||||
uploads = require('./upload'),
|
uploads = require('./upload'),
|
||||||
dataExport = require('../data/export'),
|
dataExport = require('../data/export'),
|
||||||
errors = require('../errors'),
|
|
||||||
|
|
||||||
http,
|
http,
|
||||||
formatHttpErrors,
|
|
||||||
addHeaders,
|
addHeaders,
|
||||||
cacheInvalidationHeader,
|
cacheInvalidationHeader,
|
||||||
locationHeader,
|
locationHeader,
|
||||||
|
@ -144,37 +142,6 @@ contentDispositionHeader = function contentDispositionHeader() {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* ### Format HTTP Errors
|
|
||||||
* Converts the error response from the API into a format which can be returned over HTTP
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @param {Array} error
|
|
||||||
* @return {{errors: Array, statusCode: number}}
|
|
||||||
*/
|
|
||||||
formatHttpErrors = function formatHttpErrors(error) {
|
|
||||||
var statusCode = 500,
|
|
||||||
errors = [];
|
|
||||||
|
|
||||||
if (!_.isArray(error)) {
|
|
||||||
error = [].concat(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
_.each(error, function each(errorItem) {
|
|
||||||
var errorContent = {};
|
|
||||||
|
|
||||||
// TODO: add logic to set the correct status code
|
|
||||||
statusCode = errorItem.code || 500;
|
|
||||||
|
|
||||||
errorContent.message = _.isString(errorItem) ? errorItem :
|
|
||||||
(_.isObject(errorItem) ? errorItem.message : 'Unknown API Error');
|
|
||||||
errorContent.errorType = errorItem.errorType || 'InternalServerError';
|
|
||||||
errors.push(errorContent);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {errors: errors, statusCode: statusCode};
|
|
||||||
};
|
|
||||||
|
|
||||||
addHeaders = function addHeaders(apiMethod, req, res, result) {
|
addHeaders = function addHeaders(apiMethod, req, res, result) {
|
||||||
var cacheInvalidation,
|
var cacheInvalidation,
|
||||||
location,
|
location,
|
||||||
|
@ -221,7 +188,7 @@ addHeaders = function addHeaders(apiMethod, req, res, result) {
|
||||||
* @return {Function} middleware format function to be called by the route when a matching request is made
|
* @return {Function} middleware format function to be called by the route when a matching request is made
|
||||||
*/
|
*/
|
||||||
http = function http(apiMethod) {
|
http = function http(apiMethod) {
|
||||||
return function apiHandler(req, res) {
|
return function apiHandler(req, res, next) {
|
||||||
// We define 2 properties for using as arguments in API calls:
|
// We define 2 properties for using as arguments in API calls:
|
||||||
var object = req.body,
|
var object = req.body,
|
||||||
options = _.extend({}, req.files, req.query, req.params, {
|
options = _.extend({}, req.files, req.query, req.params, {
|
||||||
|
@ -243,11 +210,9 @@ http = function http(apiMethod) {
|
||||||
}).then(function then(response) {
|
}).then(function then(response) {
|
||||||
// Send a properly formatting HTTP response containing the data with correct headers
|
// Send a properly formatting HTTP response containing the data with correct headers
|
||||||
res.json(response || {});
|
res.json(response || {});
|
||||||
}).catch(function onError(error) {
|
}).catch(function onAPIError(error) {
|
||||||
errors.logError(error);
|
// To be handled by the API middleware
|
||||||
var httpErrors = formatHttpErrors(error);
|
next(error);
|
||||||
// Send a properly formatted HTTP response containing the errors
|
|
||||||
res.status(httpErrors.statusCode).json({errors: httpErrors.errors});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -175,6 +175,37 @@ errors = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ### Format HTTP Errors
|
||||||
|
* Converts the error response from the API into a format which can be returned over HTTP
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {Array} error
|
||||||
|
* @return {{errors: Array, statusCode: number}}
|
||||||
|
*/
|
||||||
|
formatHttpErrors: function formatHttpErrors(error) {
|
||||||
|
var statusCode = 500,
|
||||||
|
errors = [];
|
||||||
|
|
||||||
|
if (!_.isArray(error)) {
|
||||||
|
error = [].concat(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
_.each(error, function each(errorItem) {
|
||||||
|
var errorContent = {};
|
||||||
|
|
||||||
|
// TODO: add logic to set the correct status code
|
||||||
|
statusCode = errorItem.code || 500;
|
||||||
|
|
||||||
|
errorContent.message = _.isString(errorItem) ? errorItem :
|
||||||
|
(_.isObject(errorItem) ? errorItem.message : 'Unknown API Error');
|
||||||
|
errorContent.errorType = errorItem.errorType || 'InternalServerError';
|
||||||
|
errors.push(errorContent);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {errors: errors, statusCode: statusCode};
|
||||||
|
},
|
||||||
|
|
||||||
handleAPIError: function (error, permsMessage) {
|
handleAPIError: function (error, permsMessage) {
|
||||||
if (!error) {
|
if (!error) {
|
||||||
return this.rejectError(
|
return this.rejectError(
|
||||||
|
@ -342,6 +373,7 @@ _.each([
|
||||||
'logErrorAndExit',
|
'logErrorAndExit',
|
||||||
'logErrorWithRedirect',
|
'logErrorWithRedirect',
|
||||||
'handleAPIError',
|
'handleAPIError',
|
||||||
|
'formatHttpErrors',
|
||||||
'renderErrorPage',
|
'renderErrorPage',
|
||||||
'error404',
|
'error404',
|
||||||
'error500'
|
'error500'
|
||||||
|
|
|
@ -5,7 +5,7 @@ function MethodNotAllowedError(message) {
|
||||||
this.message = message;
|
this.message = message;
|
||||||
this.stack = new Error().stack;
|
this.stack = new Error().stack;
|
||||||
this.code = 405;
|
this.code = 405;
|
||||||
this.type = this.name;
|
this.errorType = this.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
MethodNotAllowedError.prototype = Object.create(Error.prototype);
|
MethodNotAllowedError.prototype = Object.create(Error.prototype);
|
||||||
|
|
13
core/server/middleware/api-error-handlers.js
Normal file
13
core/server/middleware/api-error-handlers.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
var errors = require('../errors');
|
||||||
|
|
||||||
|
module.exports.methodNotAllowed = function methodNotAllowed(req, res, next) {
|
||||||
|
next(new errors.MethodNotAllowedError('Unknown method: ' + req.path));
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.errorHandler = function errorHandler(err, req, res, next) {
|
||||||
|
/*jshint unused:false */
|
||||||
|
var httpErrors = errors.formatHttpErrors(err);
|
||||||
|
errors.logError(err);
|
||||||
|
// Send a properly formatted HTTP response containing the errors
|
||||||
|
res.status(httpErrors.statusCode).json({errors: httpErrors.errors});
|
||||||
|
};
|
|
@ -217,8 +217,7 @@ function serveSharedFile(file, type, maxAge) {
|
||||||
setupMiddleware = function setupMiddleware(blogAppInstance, adminApp) {
|
setupMiddleware = function setupMiddleware(blogAppInstance, adminApp) {
|
||||||
var logging = config.logging,
|
var logging = config.logging,
|
||||||
corePath = config.paths.corePath,
|
corePath = config.paths.corePath,
|
||||||
oauthServer = oauth2orize.createServer(),
|
oauthServer = oauth2orize.createServer();
|
||||||
apiRouter;
|
|
||||||
|
|
||||||
// silence JSHint without disabling unused check for the whole file
|
// silence JSHint without disabling unused check for the whole file
|
||||||
authStrategies = authStrategies;
|
authStrategies = authStrategies;
|
||||||
|
@ -226,7 +225,7 @@ setupMiddleware = function setupMiddleware(blogAppInstance, adminApp) {
|
||||||
// Cache express server instance
|
// Cache express server instance
|
||||||
blogApp = blogAppInstance;
|
blogApp = blogAppInstance;
|
||||||
middleware.cacheBlogApp(blogApp);
|
middleware.cacheBlogApp(blogApp);
|
||||||
middleware.cacheOauthServer(oauthServer);
|
middleware.api.cacheOauthServer(oauthServer);
|
||||||
oauth.init(oauthServer, middleware.spamPrevention.resetCounter);
|
oauth.init(oauthServer, middleware.spamPrevention.resetCounter);
|
||||||
|
|
||||||
// Make sure 'req.secure' is valid for proxied requests
|
// Make sure 'req.secure' is valid for proxied requests
|
||||||
|
@ -311,19 +310,7 @@ setupMiddleware = function setupMiddleware(blogAppInstance, adminApp) {
|
||||||
|
|
||||||
// ### Routing
|
// ### Routing
|
||||||
// Set up API routes
|
// Set up API routes
|
||||||
apiRouter = routes.api(middleware);
|
blogApp.use(routes.apiBaseUri, routes.api(middleware));
|
||||||
blogApp.use(routes.apiBaseUri, apiRouter);
|
|
||||||
// ### Invalid method call on valid route
|
|
||||||
apiRouter.use(function (req, res, next) {
|
|
||||||
apiRouter.stack.forEach(function (item) {
|
|
||||||
if (item.regexp.test(req.path) && item.route !== undefined) {
|
|
||||||
return next(new errors.MethodNotAllowedError('Method not allowed'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Didn't match any path -> 404
|
|
||||||
res.status(404).json({errors: {type: 'NotFoundError', message: 'Unknown API endpoint.'}});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mount admin express app to /ghost and set up routes
|
// Mount admin express app to /ghost and set up routes
|
||||||
adminApp.use(middleware.redirectToSetup);
|
adminApp.use(middleware.redirectToSetup);
|
||||||
|
|
|
@ -18,8 +18,9 @@ var _ = require('lodash'),
|
||||||
|
|
||||||
busboy = require('./ghost-busboy'),
|
busboy = require('./ghost-busboy'),
|
||||||
cacheControl = require('./cache-control'),
|
cacheControl = require('./cache-control'),
|
||||||
spamPrevention = require('./spam-prevention'),
|
spamPrevention = require('./spam-prevention'),
|
||||||
clientAuth = require('./client-auth'),
|
clientAuth = require('./client-auth'),
|
||||||
|
apiErrorHandlers = require('./api-error-handlers'),
|
||||||
|
|
||||||
middleware,
|
middleware,
|
||||||
blogApp;
|
blogApp;
|
||||||
|
@ -306,10 +307,14 @@ middleware = {
|
||||||
module.exports = middleware;
|
module.exports = middleware;
|
||||||
module.exports.cacheBlogApp = cacheBlogApp;
|
module.exports.cacheBlogApp = cacheBlogApp;
|
||||||
|
|
||||||
module.exports.addClientSecret = clientAuth.addClientSecret;
|
module.exports.api = {
|
||||||
module.exports.cacheOauthServer = clientAuth.cacheOauthServer;
|
addClientSecret: clientAuth.addClientSecret,
|
||||||
module.exports.authenticateClient = clientAuth.authenticateClient;
|
cacheOauthServer: clientAuth.cacheOauthServer,
|
||||||
module.exports.generateAccessToken = clientAuth.generateAccessToken;
|
authenticateClient: clientAuth.authenticateClient,
|
||||||
|
generateAccessToken: clientAuth.generateAccessToken,
|
||||||
|
methodNotAllowed: apiErrorHandlers.methodNotAllowed,
|
||||||
|
errorHandler: apiErrorHandlers.errorHandler
|
||||||
|
};
|
||||||
|
|
||||||
// SSL helper functions are exported primarily for unity testing.
|
// SSL helper functions are exported primarily for unity testing.
|
||||||
module.exports.isSSLrequired = isSSLrequired;
|
module.exports.isSSLrequired = isSSLrequired;
|
||||||
|
|
|
@ -3,7 +3,7 @@ var express = require('express'),
|
||||||
api = require('../api'),
|
api = require('../api'),
|
||||||
apiRoutes;
|
apiRoutes;
|
||||||
|
|
||||||
apiRoutes = function (middleware) {
|
apiRoutes = function apiRoutes(middleware) {
|
||||||
var router = express.Router();
|
var router = express.Router();
|
||||||
// alias delete with del
|
// alias delete with del
|
||||||
router.del = router.delete;
|
router.del = router.delete;
|
||||||
|
@ -79,15 +79,20 @@ apiRoutes = function (middleware) {
|
||||||
router.get('/authentication/setup', api.http(api.authentication.isSetup));
|
router.get('/authentication/setup', api.http(api.authentication.isSetup));
|
||||||
router.post('/authentication/token',
|
router.post('/authentication/token',
|
||||||
middleware.spamPrevention.signin,
|
middleware.spamPrevention.signin,
|
||||||
middleware.addClientSecret,
|
middleware.api.addClientSecret,
|
||||||
middleware.authenticateClient,
|
middleware.api.authenticateClient,
|
||||||
middleware.generateAccessToken
|
middleware.api.generateAccessToken
|
||||||
);
|
);
|
||||||
router.post('/authentication/revoke', api.http(api.authentication.revoke));
|
router.post('/authentication/revoke', api.http(api.authentication.revoke));
|
||||||
|
|
||||||
// ## Uploads
|
// ## Uploads
|
||||||
router.post('/uploads', middleware.busboy, api.http(api.uploads.add));
|
router.post('/uploads', middleware.busboy, api.http(api.uploads.add));
|
||||||
|
|
||||||
|
// API Router middleware
|
||||||
|
router.use(middleware.api.methodNotAllowed);
|
||||||
|
|
||||||
|
router.use(middleware.api.errorHandler);
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
61
core/test/unit/middleware/api-error-handlers_spec.js
Normal file
61
core/test/unit/middleware/api-error-handlers_spec.js
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
/*globals describe, beforeEach, afterEach, it*/
|
||||||
|
/*jshint expr:true*/
|
||||||
|
var should = require('should'),
|
||||||
|
sinon = require('sinon'),
|
||||||
|
|
||||||
|
middleware = require('../../../server/middleware').middleware,
|
||||||
|
errors = require('../../../server/errors');
|
||||||
|
|
||||||
|
// To stop jshint complaining
|
||||||
|
should.equal(true, true);
|
||||||
|
|
||||||
|
describe('Middleware: API Error Handlers', function () {
|
||||||
|
var sandbox, req, res, next;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
sandbox = sinon.sandbox.create();
|
||||||
|
req = {};
|
||||||
|
res = {};
|
||||||
|
res.json = sandbox.spy();
|
||||||
|
res.status = sandbox.stub().returns(res);
|
||||||
|
next = sandbox.spy();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
sandbox.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('methodNotAllowed', function () {
|
||||||
|
it('calls next with an error', function () {
|
||||||
|
req.path = 'test';
|
||||||
|
|
||||||
|
middleware.api.methodNotAllowed(req, res, next);
|
||||||
|
|
||||||
|
next.calledOnce.should.be.true;
|
||||||
|
next.firstCall.args[0].code.should.equal(405);
|
||||||
|
next.firstCall.args[0].errorType.should.equal('MethodNotAllowedError');
|
||||||
|
next.firstCall.args[0].message.should.match(/test$/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('errorHandler', function () {
|
||||||
|
it('sends a JSON error response', function () {
|
||||||
|
errors.logError = sandbox.spy(errors, 'logError');
|
||||||
|
errors.formatHttpErrors = sandbox.spy(errors, 'formatHttpErrors');
|
||||||
|
|
||||||
|
var msg = 'Something got lost',
|
||||||
|
err = new errors.NotFoundError(msg);
|
||||||
|
|
||||||
|
middleware.api.errorHandler(err, req, res, next);
|
||||||
|
|
||||||
|
next.called.should.be.false;
|
||||||
|
errors.logError.calledOnce.should.be.true;
|
||||||
|
errors.formatHttpErrors.calledOnce.should.be.true;
|
||||||
|
|
||||||
|
res.status.calledWith(404).should.be.true;
|
||||||
|
res.json.calledOnce.should.be.true;
|
||||||
|
res.json.firstCall.args[0].errors[0].message.should.eql(msg);
|
||||||
|
res.json.firstCall.args[0].errors[0].errorType.should.eql('NotFoundError');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -20,7 +20,7 @@ describe('Middleware: Client Auth', function () {
|
||||||
|
|
||||||
req.body = requestBody;
|
req.body = requestBody;
|
||||||
|
|
||||||
middleware.addClientSecret(req, res, next);
|
middleware.api.addClientSecret(req, res, next);
|
||||||
|
|
||||||
next.called.should.be.true;
|
next.called.should.be.true;
|
||||||
should(req.body).have.property('client_secret');
|
should(req.body).have.property('client_secret');
|
||||||
|
@ -34,7 +34,7 @@ describe('Middleware: Client Auth', function () {
|
||||||
|
|
||||||
req.body = requestBody;
|
req.body = requestBody;
|
||||||
|
|
||||||
middleware.addClientSecret(req, res, next);
|
middleware.api.addClientSecret(req, res, next);
|
||||||
|
|
||||||
next.called.should.be.true;
|
next.called.should.be.true;
|
||||||
should(req.body).have.property('client_secret');
|
should(req.body).have.property('client_secret');
|
|
@ -1,8 +1,12 @@
|
||||||
/*globals describe, beforeEach, afterEach, it*/
|
/*globals describe, beforeEach, afterEach, it*/
|
||||||
/*jshint expr:true*/
|
/*jshint expr:true*/
|
||||||
var sinon = require('sinon'),
|
var should = require('should'),
|
||||||
|
sinon = require('sinon'),
|
||||||
decideIsAdmin = require('../../../server/middleware/decide-is-admin');
|
decideIsAdmin = require('../../../server/middleware/decide-is-admin');
|
||||||
|
|
||||||
|
// To stop jshint complaining
|
||||||
|
should.equal(true, true);
|
||||||
|
|
||||||
describe('Middleware: decideIsAdmin', function () {
|
describe('Middleware: decideIsAdmin', function () {
|
||||||
var sandbox,
|
var sandbox,
|
||||||
res,
|
res,
|
||||||
|
|
Loading…
Add table
Reference in a new issue