From 9540bc6178030265ef701a829124fcdb03729f05 Mon Sep 17 00:00:00 2001 From: hippoz <10706925-hippoz@users.noreply.gitlab.com> Date: Fri, 5 Aug 2022 05:18:55 +0300 Subject: [PATCH] add hacky database error handling to prevent the server from crashing due to trivial errors --- src/auth.ts | 2 +- src/database/index.ts | 24 ++++++++++++++++++++++-- src/errors.ts | 1 + src/gateway/index.ts | 4 ++++ src/routes/api/v1/channels.ts | 20 ++++++++++---------- src/routes/api/v1/messages.ts | 10 +++++----- src/routes/api/v1/users.ts | 8 ++++---- 7 files changed, 47 insertions(+), 22 deletions(-) diff --git a/src/auth.ts b/src/auth.ts index 46318d6..cd5238b 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -83,7 +83,7 @@ export function decodeToken(encoded: string): Promise { } const user = await query("SELECT * FROM users WHERE id = $1", [decoded.id]); - if (user.rowCount < 1) { + if (!user || user.rowCount < 1) { reject("user does not exist (could not find in database by id)"); return; } diff --git a/src/database/index.ts b/src/database/index.ts index 06e4def..485285f 100644 --- a/src/database/index.ts +++ b/src/database/index.ts @@ -1,5 +1,25 @@ -import { Pool } from "pg"; +import { Pool, QueryResult } from "pg"; const pool = new Pool(); -export const query = pool.query.bind(pool); +// hacky wrapper function that returns null on database errors. +// this is done because express doesn't automatically call next() +// when an async function throws, so this prevents the server +// from crashing due to trivial database errors that can be handled. +// we could use a try catch block for each query, but that will +// quickly get cumbersome. +export const query = function(text: string, params: any[] = [], rejectOnError = false): Promise { + return new Promise((resolve, reject) => { + pool.query(text, params) + .then((data) => { + resolve(data); + }) + .catch((error) => { + if (rejectOnError) { + reject(error); + } else { + resolve(null); + } + }); + }); +}; diff --git a/src/errors.ts b/src/errors.ts index 1d236c6..ea2733b 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -20,4 +20,5 @@ export const gatewayErrors = { PAYLOAD_TOO_LARGE: { code: 4007, message: "Payload too large" }, TOO_MANY_SESSIONS: { code: 4008, message: "Too many sessions" }, NOT_AUTHENTICATED: { code: 4009, message: "Not authenticated" }, + GOT_NO_DATABASE_DATA: { code: 4010, message: "Unexpectedly got no data from database" }, }; diff --git a/src/gateway/index.ts b/src/gateway/index.ts index 99990a0..8f039cb 100644 --- a/src/gateway/index.ts +++ b/src/gateway/index.ts @@ -223,6 +223,10 @@ export default function(server: Server) { // TODO: each user should have their own list of channels that they join const channels = await query("SELECT id, name, owner_id FROM channels ORDER BY id ASC"); + if (!channels) { + return closeWithError(ws, gatewayErrors.GOT_NO_DATABASE_DATA); + } + clientSubscribe(ws, "*"); channels.rows.forEach(c => { clientSubscribe(ws, `channel:${c.id}`); diff --git a/src/routes/api/v1/channels.ts b/src/routes/api/v1/channels.ts index 17d2cfa..9a21017 100644 --- a/src/routes/api/v1/channels.ts +++ b/src/routes/api/v1/channels.ts @@ -26,7 +26,7 @@ router.post( const { name } = req.body; const result = await query("INSERT INTO channels(name, owner_id) VALUES ($1, $2) RETURNING id, name, owner_id", [name, req.user.id]); - if (result.rowCount < 1) { + if (!result || result.rowCount < 1) { return res.status(500).json({ ...errors.GOT_NO_DATABASE_DATA }); @@ -60,7 +60,7 @@ router.put( const id = parseInt(req.params.id); // TODO: ?? const permissionCheckResult = await query("SELECT owner_id FROM channels WHERE id = $1", [id]); - if (permissionCheckResult.rowCount < 1) { + if (!permissionCheckResult || permissionCheckResult.rowCount < 1) { return res.status(404).json({ ...errors.NOT_FOUND }); @@ -72,7 +72,7 @@ router.put( } const result = await query("UPDATE channels SET name = $1 WHERE id = $2", [name, id]); - if (result.rowCount < 1) { + if (!result || result.rowCount < 1) { return res.status(500).json({ ...errors.GOT_NO_DATABASE_DATA }); @@ -106,7 +106,7 @@ router.delete( const id = parseInt(req.params.id); // TODO: ?? const permissionCheckResult = await query("SELECT owner_id FROM channels WHERE id = $1", [id]); - if (permissionCheckResult.rowCount < 1) { + if (!permissionCheckResult || permissionCheckResult.rowCount < 1) { return res.status(404).json({ ...errors.NOT_FOUND }); @@ -118,7 +118,7 @@ router.delete( } const result = await query("DELETE FROM channels WHERE id = $1", [id]); - if (result.rowCount < 1) { + if (!result || result.rowCount < 1) { return res.status(500).json({ ...errors.GOT_NO_DATABASE_DATA }); @@ -148,7 +148,7 @@ router.get( const { id } = req.params; const result = await query("SELECT id, name, owner_id FROM channels WHERE id = $1", [id]); - if (result.rowCount < 1) { + if (!result || result.rowCount < 1) { return res.status(404).json({ ...errors.NOT_FOUND }); @@ -164,7 +164,7 @@ router.get( async (req, res) => { const result = await query("SELECT id, name, owner_id FROM channels"); - return res.status(200).send(result.rows); + return res.status(200).send(result ? result.rows : []); } ); @@ -185,7 +185,7 @@ router.post( const createdAt = Date.now().toString(); const result = await query("INSERT INTO messages(content, channel_id, author_id, created_at) VALUES ($1, $2, $3, $4) RETURNING id", [content, channelId, authorId, createdAt]); - if (result.rowCount < 1) { + if (!result || result.rowCount < 1) { return res.status(500).json({ ...errors.GOT_NO_DATABASE_DATA }); @@ -226,10 +226,10 @@ router.get( if (before) { const result = await query(getMessagesByChannelPage, [before, channelId]); - finalRows = result.rows; + finalRows = result ? result.rows : []; } else { const result = await query(getMessagesByChannelFirstPage, [channelId]); - finalRows = result.rows; + finalRows = result ? result.rows : []; } return res.status(200).send(finalRows); diff --git a/src/routes/api/v1/messages.ts b/src/routes/api/v1/messages.ts index d623014..1c14be7 100644 --- a/src/routes/api/v1/messages.ts +++ b/src/routes/api/v1/messages.ts @@ -22,7 +22,7 @@ router.delete( const id = parseInt(req.params.id); // TODO: ?? const permissionCheckResult = await query("SELECT author_id, channel_id FROM messages WHERE id = $1", [id]); - if (permissionCheckResult.rowCount < 1) { + if (!permissionCheckResult || permissionCheckResult.rowCount < 1) { return res.status(404).json({ ...errors.NOT_FOUND }); @@ -34,7 +34,7 @@ router.delete( } const result = await query("DELETE FROM messages WHERE id = $1", [id]); - if (result.rowCount < 1) { + if (!result || result.rowCount < 1) { return res.status(500).json({ ...errors.GOT_NO_DATABASE_DATA }); @@ -67,7 +67,7 @@ router.put( const id = parseInt(req.params.id); // TODO: ?? const permissionCheckResult = await query(getMessageById, [id]); - if (permissionCheckResult.rowCount < 1) { + if (!permissionCheckResult || permissionCheckResult.rowCount < 1) { return res.status(404).json({ ...errors.NOT_FOUND }); @@ -79,7 +79,7 @@ router.put( } const result = await query("UPDATE messages SET content = $1 WHERE id = $2", [content, id]); - if (result.rowCount < 1) { + if (!result || result.rowCount < 1) { return res.status(500).json({ ...errors.GOT_NO_DATABASE_DATA }); @@ -111,7 +111,7 @@ router.get( const { id } = req.params; const result = await query(getMessageById, [id]); - if (result.rowCount < 1) { + if (!result || result.rowCount < 1) { return res.status(404).json({ ...errors.NOT_FOUND }); diff --git a/src/routes/api/v1/users.ts b/src/routes/api/v1/users.ts index 5f12435..817ddd4 100644 --- a/src/routes/api/v1/users.ts +++ b/src/routes/api/v1/users.ts @@ -26,7 +26,7 @@ router.post( const { username, password } = req.body; const existingUser = await query("SELECT * FROM users WHERE username = $1", [username]); - if (existingUser.rowCount > 0) { + if (existingUser && existingUser.rowCount > 0) { return res.status(400).json({ ...errors.INVALID_DATA, errors: [ { location: "body", msg: "Username already exists", param: "username" } ] @@ -35,7 +35,7 @@ router.post( const hashedPassword = await hash(password, 10); const insertedUser = await query("INSERT INTO users(username, password, is_superuser) VALUES ($1, $2, $3) RETURNING id, username, is_superuser", [username, hashedPassword, false]); - if (insertedUser.rowCount < 1) { + if (!insertedUser || insertedUser.rowCount < 1) { return res.status(500).json({ ...errors.GOT_NO_DATABASE_DATA }); @@ -58,7 +58,7 @@ router.post( const { username, password } = req.body; const existingUser = await query("SELECT * FROM users WHERE username = $1", [username]); - if (existingUser.rowCount < 1) { + if (!existingUser || existingUser.rowCount < 1) { return res.status(400).json({ ...errors.BAD_LOGIN_CREDENTIALS }); } @@ -95,7 +95,7 @@ router.post( if (superuserKey && superuserKey.length >= 1 && key === superuserKey && req.user) { const updateUserResult = await query("UPDATE users SET is_superuser = true WHERE id = $1", [req.user.id]); - if (updateUserResult.rowCount < 1) { + if (!updateUserResult || updateUserResult.rowCount < 1) { return res.status(500).json({ ...errors.GOT_NO_DATABASE_DATA });