Add Express.js boilerplate and consistent error handling middleware
Changes:
- Convert router.ts to app.ts and routers/auth-router.ts to separate routers from Express.js app
- Remove signal listeners as they do not work for the Jami daemon
- Add 'error' and 'listening' event listeners to the HTTP server to show useful info on startup
- Add error handling middleware to return error strings rather than HTML
Change-Id: Icc0410a9fc596a419be024c6b0503ca4183f7563
diff --git a/server/src/app.ts b/server/src/app.ts
new file mode 100644
index 0000000..f126ed4
--- /dev/null
+++ b/server/src/app.ts
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2022 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program. If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+import express, { NextFunction, Request, Response } from 'express';
+import log from 'loglevel';
+import { Service } from 'typedi';
+
+import { StatusCode } from './constants.js';
+import { AuthRouter } from './routers/auth-router.js';
+
+@Service()
+class App {
+ constructor(private authRouter: AuthRouter) {}
+
+ async build() {
+ const app = express();
+
+ // Setup routing
+ const authRouter = await this.authRouter.build();
+ app.use('/auth', authRouter);
+
+ // Setup 404 error handling
+ app.use((_req, res) => {
+ res.sendStatus(StatusCode.NOT_FOUND);
+ });
+
+ // Setup internal error handling
+ app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
+ log.error(err);
+ res.status(StatusCode.INTERNAL_SERVER_ERROR).send(err.message);
+ });
+
+ return app;
+ }
+}
+
+export { App };
diff --git a/server/src/index.ts b/server/src/index.ts
index 587438c..661693f 100644
--- a/server/src/index.ts
+++ b/server/src/index.ts
@@ -22,31 +22,48 @@
import log from 'loglevel';
import { Container } from 'typedi';
-import { Router } from './router.js';
+import { App } from './app.js';
import { Ws } from './ws.js';
log.setLevel(process.env.NODE_ENV === 'production' ? 'error' : 'trace');
-const app = await Container.get(Router).build();
+const port: string | number = 5000;
+
+const app = await Container.get(App).build();
const wss = await Container.get(Ws).build();
-// Disable HTTP 1.1 Keep-Alive
-const server = createServer((_, res) => res.setHeader('Connection', 'close'));
+const server = createServer();
+
server.on('request', app);
server.on('upgrade', wss);
-server.listen({
- host: '0.0.0.0',
- port: 5000,
- exclusive: true,
-});
-log.debug('Server started (HTTP + WS)');
+server.listen(port);
+server.on('error', onError);
+server.on('listening', onListening);
-const closeFn: NodeJS.SignalsListener = (signal) => {
- log.info(signal);
- server.close();
- log.info('server closed');
-};
-process.once('SIGTERM', closeFn);
-process.once('SIGHUP', closeFn);
-process.once('SIGINT', closeFn);
+function onError(error: NodeJS.ErrnoException) {
+ if (error.syscall !== 'listen') {
+ throw error;
+ }
+
+ const bind = typeof port === 'string' ? `Pipe ${port}` : `Port ${port}`;
+
+ switch (error.code) {
+ case 'EACCESS':
+ log.error(bind + ' requires elevated privileges');
+ process.exit(1);
+ break;
+ case 'EADDRINUSE':
+ log.error(bind + ' is already in use');
+ process.exit(1);
+ break;
+ default:
+ throw error;
+ }
+}
+
+function onListening() {
+ const address = server.address();
+ const bind = typeof address === 'string' ? `pipe ${address}` : `port ${address?.port}`;
+ log.debug('Listening on ' + bind);
+}
diff --git a/server/src/router.ts b/server/src/routers/auth-router.ts
similarity index 80%
rename from server/src/router.ts
rename to server/src/routers/auth-router.ts
index 0b00780..dec19b3 100644
--- a/server/src/router.ts
+++ b/server/src/routers/auth-router.ts
@@ -15,20 +15,16 @@
* License along with this program. If not, see
* <https://www.gnu.org/licenses/>.
*/
-import express, { json, Router as ERouter } from 'express';
+import { Router } from 'express';
import asyncHandler from 'express-async-handler';
import { Service } from 'typedi';
-import { StatusCode } from './constants.js';
+import { StatusCode } from '../constants.js';
@Service()
-class Router {
+class AuthRouter {
async build() {
- const router = ERouter();
-
- router.use(json());
-
- await Promise.resolve(42);
+ const router = Router();
router.post(
'/new-account',
@@ -46,10 +42,8 @@
})
);
- const app = express();
- app.use('/', router);
- return app;
+ return router;
}
}
-export { Router };
+export { AuthRouter };