From 813682f640ba27d7fd5fb868e1211fe6b1fbc2bb Mon Sep 17 00:00:00 2001 From: Jake Bromberg Date: Sat, 21 Feb 2026 08:27:24 -0800 Subject: [PATCH 1/2] fix: standardize error response format to { message } across all endpoints Mixed formats (plain strings, { status, message }, leaked error.message) are now consistently { message: '...' }. Internal errors no longer expose raw error messages to clients. Co-authored-by: Cursor --- apps/backend/controllers/djs.controller.ts | 10 ++-- .../controllers/flowsheet.controller.ts | 16 +++--- .../backend/controllers/library.controller.ts | 37 ++++++------ apps/backend/middleware/errorHandler.ts | 3 +- tests/unit/middleware/errorHandler.test.ts | 56 +++++++++++++++++++ 5 files changed, 88 insertions(+), 34 deletions(-) create mode 100644 tests/unit/middleware/errorHandler.test.ts diff --git a/apps/backend/controllers/djs.controller.ts b/apps/backend/controllers/djs.controller.ts index 1e160cfd..72ad9155 100644 --- a/apps/backend/controllers/djs.controller.ts +++ b/apps/backend/controllers/djs.controller.ts @@ -12,7 +12,7 @@ export type binBody = { export const addToBin: RequestHandler = async (req, res, next) => { if (req.body.album_id === undefined || req.body.dj_id === undefined) { console.error('Bad Request, Missing Album Identifier: album_id'); - res.status(400).send('Bad Request, Missing DJ or album identifier: album_id'); + res.status(400).json({ message: 'Bad Request, Missing DJ or album identifier: album_id' }); } else { const bin_entry: NewBinEntry = { dj_id: req.body.dj_id, @@ -40,7 +40,7 @@ export type binQuery = { export const deleteFromBin: RequestHandler = async (req, res, next) => { if (req.query.album_id === undefined || req.query.dj_id === undefined) { console.error('Bad Request, Missing Bin Entry Identifier: album_id or dj_id'); - res.status(400).send('Bad Request, Missing Bin Entry Identifier: album_id or dj_id'); + res.status(400).json({ message: 'Bad Request, Missing Bin Entry Identifier: album_id or dj_id' }); } else { try { //check that the dj_id === dj_id of bin entry @@ -56,7 +56,7 @@ export const deleteFromBin: RequestHandler = export const getBin: RequestHandler = async (req, res, next) => { if (req.query.dj_id === undefined) { console.error('Bad Request, Missing DJ Identifier: dj_id'); - res.status(400).send('Bad Request, Missing DJ Identifier: dj_id'); + res.status(400).json({ message: 'Bad Request, Missing DJ Identifier: dj_id' }); } else { try { const dj_bin = await DJService.getBinFromDB(req.query.dj_id); @@ -72,7 +72,7 @@ export const getBin: RequestHandler export const getPlaylistsForDJ: RequestHandler = async (req, res, next) => { if (req.query.dj_id === undefined) { console.error('Bad Request, Missing DJ Identifier: dj_id'); - res.status(400).send('Bad Request, Missing DJ Identifier: dj_id'); + res.status(400).json({ message: 'Bad Request, Missing DJ Identifier: dj_id' }); } else { try { const playlists = await DJService.getPlaylistsForDJ(req.query.dj_id); @@ -88,7 +88,7 @@ export const getPlaylistsForDJ: RequestHandler = async (req, res, next) => { if (req.query.playlist_id === undefined) { console.error('Bad Request, Missing Playlist Identifier: playlist_id'); - res.status(400).send('Bad Request, Missing Playlist Identifier: playlist_id'); + res.status(400).json({ message: 'Bad Request, Missing Playlist Identifier: playlist_id' }); } else { try { const playlist = await DJService.getPlaylist(parseInt(req.query.playlist_id)); diff --git a/apps/backend/controllers/flowsheet.controller.ts b/apps/backend/controllers/flowsheet.controller.ts index c8074c56..b6547824 100644 --- a/apps/backend/controllers/flowsheet.controller.ts +++ b/apps/backend/controllers/flowsheet.controller.ts @@ -109,7 +109,7 @@ export const getLatest: RequestHandler = async (req, res, next) => { res.status(200).json(latest[0]); } else { console.error('No Tracks found'); - res.status(404).send('Error: No Tracks found'); + res.status(404).json({ message: 'No Tracks found' }); } } catch (e) { console.error('Error: Failed to retrieve track'); @@ -142,13 +142,13 @@ export const addEntry: RequestHandler = async (req: Request const { entry_id } = req.body; if (entry_id === undefined) { console.error('Bad Request, Missing entry identifier: entry_id'); - res.status(400).send('Bad Request, Missing entry identifier: entry_id'); + res.status(400).json({ message: 'Bad Request, Missing entry identifier: entry_id' }); } else { try { const removedEntry: FSEntry = await flowsheet_service.removeTrack(entry_id); @@ -273,7 +273,7 @@ export const updateEntry: RequestHandler, res, next) => { const current_show = await flowsheet_service.getLatestShow(); if (req.body.dj_id === undefined) { - res.status(400).send('Bad Request, Must include a dj_id to join show'); + res.status(400).json({ message: 'Bad Request, Must include a dj_id to join show' }); } else if (current_show?.end_time !== null) { try { const show_session: Show = await flowsheet_service.startShow( @@ -416,7 +416,7 @@ export const getShowInfo: RequestHandler { export type RotationAddRequest = Omit; export const addRotation: RequestHandler = async (req, res, next) => { if (req.body.album_id === undefined || req.body.rotation_bin === undefined) { - res.status(400).send('Missing Parameters: album_id or rotation_bin'); + res.status(400).json({ message: 'Missing Parameters: album_id or rotation_bin' }); } else { try { const rotationRelease: RotationRelease = await libraryService.addToRotation(req.body); @@ -187,16 +184,16 @@ export const killRotation: RequestHandler const { body } = req; if (body.rotation_id === undefined) { - res.status(400).send('Bad Request, Missing Parameter: rotation_id'); + res.status(400).json({ message: 'Bad Request, Missing Parameter: rotation_id' }); } else if (body.kill_date !== undefined && !libraryService.isISODate(body.kill_date)) { - res.status(400).send('Bad Request, Incorrect Date Format: kill_date should be of form YYYY-MM-DD'); + res.status(400).json({ message: 'Bad Request, Incorrect Date Format: kill_date should be of form YYYY-MM-DD' }); } else { try { const updatedRotation: RotationRelease = await libraryService.killRotationInDB(body.rotation_id, body.kill_date); if (updatedRotation !== undefined) { res.status(200).json(updatedRotation); } else { - res.status(400).json({ status: 400, message: 'Rotation entry not found' }); + res.status(400).json({ message: 'Rotation entry not found' }); } } catch (e) { console.error('Failed to update rotation kill_date'); @@ -220,7 +217,7 @@ export const getFormats: RequestHandler = async (req, res, next) => { export const addFormat: RequestHandler = async (req, res, next) => { const { body } = req; if (body.name === undefined) { - res.status(400).send('Bad Request, Missing Parameter: name'); + res.status(400).json({ message: 'Bad Request, Missing Parameter: name' }); } else { try { const newFormat: NewAlbumFormat = { @@ -245,7 +242,7 @@ export const getGenres: RequestHandler = async (req, res) => { export const addGenre: RequestHandler = async (req, res, next) => { const { body } = req; if (body.name === undefined || body.description === undefined) { - res.status(400).send('Bad Request, Parameters name and description are required.'); + res.status(400).json({ message: 'Bad Request, Parameters name and description are required.' }); } else { try { const newGenre: NewGenre = { @@ -271,7 +268,7 @@ export const getAlbum: RequestHandler { + it('returns { message } with correct status for WxycError', () => { + const res = mockResponse(); + const error = new WxycError('Album not found', 404); + + errorHandler(error, mockReq, res, mockNext); + + expect(res.status).toHaveBeenCalledWith(404); + expect(res.json).toHaveBeenCalledWith({ message: 'Album not found' }); + }); + + it('returns generic message for non-WxycError (does not leak internals)', () => { + const res = mockResponse(); + const error = new Error('SELECT * FROM users failed: connection refused'); + + errorHandler(error, mockReq, res, mockNext); + + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ message: 'Internal server error' }); + }); + + it('handles non-Error values thrown', () => { + const res = mockResponse(); + + errorHandler('something broke', mockReq, res, mockNext); + + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ message: 'Internal server error' }); + }); + + it('logs non-WxycError errors to console', () => { + const res = mockResponse(); + const error = new Error('db connection lost'); + const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); + + errorHandler(error, mockReq, res, mockNext); + + expect(consoleSpy).toHaveBeenCalledWith('Unhandled error:', error); + consoleSpy.mockRestore(); + }); +}); From b78cb87030f5f242a2cffe0e194de7bc31121029 Mon Sep 17 00:00:00 2001 From: Jake Bromberg Date: Fri, 27 Feb 2026 14:01:15 -0800 Subject: [PATCH 2/2] fix: resolve lint errors --- .../controllers/flowsheet.controller.ts | 4 ++- tests/unit/middleware/errorHandler.test.ts | 32 ++++++++++--------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/apps/backend/controllers/flowsheet.controller.ts b/apps/backend/controllers/flowsheet.controller.ts index b6547824..c4d056ab 100644 --- a/apps/backend/controllers/flowsheet.controller.ts +++ b/apps/backend/controllers/flowsheet.controller.ts @@ -189,7 +189,9 @@ export const addEntry: RequestHandler = async (req: Request { it('returns { message } with correct status for WxycError', () => { - const res = mockResponse(); + const { res, statusMock, jsonMock } = mockResponse(); const error = new WxycError('Album not found', 404); errorHandler(error, mockReq, res, mockNext); - expect(res.status).toHaveBeenCalledWith(404); - expect(res.json).toHaveBeenCalledWith({ message: 'Album not found' }); + expect(statusMock).toHaveBeenCalledWith(404); + expect(jsonMock).toHaveBeenCalledWith({ message: 'Album not found' }); }); it('returns generic message for non-WxycError (does not leak internals)', () => { - const res = mockResponse(); + const { res, statusMock, jsonMock } = mockResponse(); const error = new Error('SELECT * FROM users failed: connection refused'); errorHandler(error, mockReq, res, mockNext); - expect(res.status).toHaveBeenCalledWith(500); - expect(res.json).toHaveBeenCalledWith({ message: 'Internal server error' }); + expect(statusMock).toHaveBeenCalledWith(500); + expect(jsonMock).toHaveBeenCalledWith({ message: 'Internal server error' }); }); it('handles non-Error values thrown', () => { - const res = mockResponse(); + const { res, statusMock, jsonMock } = mockResponse(); errorHandler('something broke', mockReq, res, mockNext); - expect(res.status).toHaveBeenCalledWith(500); - expect(res.json).toHaveBeenCalledWith({ message: 'Internal server error' }); + expect(statusMock).toHaveBeenCalledWith(500); + expect(jsonMock).toHaveBeenCalledWith({ message: 'Internal server error' }); }); it('logs non-WxycError errors to console', () => { - const res = mockResponse(); + const { res } = mockResponse(); const error = new Error('db connection lost'); const consoleSpy = jest.spyOn(console, 'error').mockImplementation();