Skip to content

Commit fb9e3a8

Browse files
Jake Brombergcursoragent
andcommitted
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 <cursoragent@cursor.com>
1 parent 9441273 commit fb9e3a8

5 files changed

Lines changed: 88 additions & 34 deletions

File tree

apps/backend/controllers/djs.controller.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export type binBody = {
1212
export const addToBin: RequestHandler<object, unknown, binBody> = async (req, res, next) => {
1313
if (req.body.album_id === undefined || req.body.dj_id === undefined) {
1414
console.error('Bad Request, Missing Album Identifier: album_id');
15-
res.status(400).send('Bad Request, Missing DJ or album identifier: album_id');
15+
res.status(400).json({ message: 'Bad Request, Missing DJ or album identifier: album_id' });
1616
} else {
1717
const bin_entry: NewBinEntry = {
1818
dj_id: req.body.dj_id,
@@ -40,7 +40,7 @@ export type binQuery = {
4040
export const deleteFromBin: RequestHandler<object, unknown, unknown, binQuery> = async (req, res, next) => {
4141
if (req.query.album_id === undefined || req.query.dj_id === undefined) {
4242
console.error('Bad Request, Missing Bin Entry Identifier: album_id or dj_id');
43-
res.status(400).send('Bad Request, Missing Bin Entry Identifier: album_id or dj_id');
43+
res.status(400).json({ message: 'Bad Request, Missing Bin Entry Identifier: album_id or dj_id' });
4444
} else {
4545
try {
4646
//check that the dj_id === dj_id of bin entry
@@ -56,7 +56,7 @@ export const deleteFromBin: RequestHandler<object, unknown, unknown, binQuery> =
5656
export const getBin: RequestHandler<object, unknown, object, { dj_id: string }> = async (req, res, next) => {
5757
if (req.query.dj_id === undefined) {
5858
console.error('Bad Request, Missing DJ Identifier: dj_id');
59-
res.status(400).send('Bad Request, Missing DJ Identifier: dj_id');
59+
res.status(400).json({ message: 'Bad Request, Missing DJ Identifier: dj_id' });
6060
} else {
6161
try {
6262
const dj_bin = await DJService.getBinFromDB(req.query.dj_id);
@@ -72,7 +72,7 @@ export const getBin: RequestHandler<object, unknown, object, { dj_id: string }>
7272
export const getPlaylistsForDJ: RequestHandler<object, unknown, object, { dj_id: string }> = async (req, res, next) => {
7373
if (req.query.dj_id === undefined) {
7474
console.error('Bad Request, Missing DJ Identifier: dj_id');
75-
res.status(400).send('Bad Request, Missing DJ Identifier: dj_id');
75+
res.status(400).json({ message: 'Bad Request, Missing DJ Identifier: dj_id' });
7676
} else {
7777
try {
7878
const playlists = await DJService.getPlaylistsForDJ(req.query.dj_id);
@@ -88,7 +88,7 @@ export const getPlaylistsForDJ: RequestHandler<object, unknown, object, { dj_id:
8888
export const getPlaylist: RequestHandler<object, unknown, object, { playlist_id: string }> = async (req, res, next) => {
8989
if (req.query.playlist_id === undefined) {
9090
console.error('Bad Request, Missing Playlist Identifier: playlist_id');
91-
res.status(400).send('Bad Request, Missing Playlist Identifier: playlist_id');
91+
res.status(400).json({ message: 'Bad Request, Missing Playlist Identifier: playlist_id' });
9292
} else {
9393
try {
9494
const playlist = await DJService.getPlaylist(parseInt(req.query.playlist_id));

apps/backend/controllers/flowsheet.controller.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ export const getLatest: RequestHandler = async (req, res, next) => {
109109
res.status(200).json(latest[0]);
110110
} else {
111111
console.error('No Tracks found');
112-
res.status(404).send('Error: No Tracks found');
112+
res.status(404).json({ message: 'No Tracks found' });
113113
}
114114
} catch (e) {
115115
console.error('Error: Failed to retrieve track');
@@ -142,13 +142,13 @@ export const addEntry: RequestHandler = async (req: Request<object, object, FSEn
142142
}
143143
if (latestShow?.end_time !== null) {
144144
console.error('Bad Request, There are no active shows');
145-
res.status(400).send('Bad Request, There are no active shows');
145+
res.status(400).json({ message: 'Bad Request, There are no active shows' });
146146
} else {
147147
if (body.message === undefined) {
148148
// no message passed, so we assume we're adding a track to the flowsheet
149149
if (body.track_title === undefined) {
150150
console.error('Bad Request, Missing query parameter: track_title');
151-
res.status(400).send('Bad Request, Missing query parameter: track_title');
151+
res.status(400).json({ message: 'Bad Request, Missing query parameter: track_title' });
152152
} else {
153153
try {
154154
if (body.album_id !== undefined) {
@@ -189,7 +189,7 @@ export const addEntry: RequestHandler = async (req: Request<object, object, FSEn
189189
body.track_title === undefined
190190
) {
191191
console.error('Bad Request, Missing Flowsheet Parameters: album_title, artist_name, track_title');
192-
res.status(400).send('Bad Request, Missing Flowsheet Parameters: album_title, artist_name, track_title');
192+
res.status(400).json({ message: 'Bad Request, Missing Flowsheet Parameters: album_title, artist_name, track_title' });
193193
} else {
194194
const fsEntry: NewFSEntry = {
195195
...body,
@@ -243,7 +243,7 @@ export const deleteEntry: RequestHandler<object, unknown, { entry_id: number }>
243243
const { entry_id } = req.body;
244244
if (entry_id === undefined) {
245245
console.error('Bad Request, Missing entry identifier: entry_id');
246-
res.status(400).send('Bad Request, Missing entry identifier: entry_id');
246+
res.status(400).json({ message: 'Bad Request, Missing entry identifier: entry_id' });
247247
} else {
248248
try {
249249
const removedEntry: FSEntry = await flowsheet_service.removeTrack(entry_id);
@@ -273,7 +273,7 @@ export const updateEntry: RequestHandler<object, unknown, { entry_id: number; da
273273
const { entry_id, data } = req.body;
274274
if (entry_id === undefined) {
275275
console.error('Bad Request, Missing entry identifier: entry_id');
276-
res.status(400).send('Bad Request, Missing entry identifier: entry_id');
276+
res.status(400).json({ message: 'Bad Request, Missing entry identifier: entry_id' });
277277
} else {
278278
try {
279279
const updatedEntry: FSEntry = await flowsheet_service.updateEntry(entry_id, data);
@@ -296,7 +296,7 @@ export type JoinRequestBody = {
296296
export const joinShow: RequestHandler = async (req: Request<object, object, JoinRequestBody>, res, next) => {
297297
const current_show = await flowsheet_service.getLatestShow();
298298
if (req.body.dj_id === undefined) {
299-
res.status(400).send('Bad Request, Must include a dj_id to join show');
299+
res.status(400).json({ message: 'Bad Request, Must include a dj_id to join show' });
300300
} else if (current_show?.end_time !== null) {
301301
try {
302302
const show_session: Show = await flowsheet_service.startShow(
@@ -416,7 +416,7 @@ export const getShowInfo: RequestHandler<object, unknown, object, { show_id: str
416416
const showId = parseInt(req.query.show_id);
417417
if (isNaN(showId)) {
418418
console.error('Bad Request, Missing Show Identifier: show_id');
419-
res.status(400).send('Bad Request, Missing Show Identifier: show_id');
419+
res.status(400).json({ message: 'Bad Request, Missing Show Identifier: show_id' });
420420
} else {
421421
try {
422422
const showInfo = await flowsheet_service.getPlaylist(showId);

apps/backend/controllers/library.controller.ts

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ export const addAlbum: RequestHandler = async (req: Request<object, object, NewA
3434
(body.artist_name === undefined && body.artist_id === undefined)
3535
) {
3636
res.status(400).json({
37-
status: 400,
3837
message: 'Missing Parameters: album_title, label, genre_id, format_id, artist_name, or artist_id',
3938
});
4039
} else {
@@ -49,10 +48,10 @@ export const addAlbum: RequestHandler = async (req: Request<object, object, NewA
4948
}
5049
}
5150
if (!artist_id) {
52-
res.status(400);
53-
res.send(
54-
"Artist doesn't exist or hasn't released an album in this genre before. Add a new artist entry to the library"
55-
);
51+
res.status(400).json({
52+
message:
53+
"Artist doesn't exist or hasn't released an album in this genre before. Add a new artist entry to the library",
54+
});
5655
} else {
5756
try {
5857
const new_album: NewAlbum = {
@@ -98,14 +97,13 @@ export const searchForAlbum: RequestHandler = async (
9897
query.album_title === undefined &&
9998
(query.code_letters === undefined || query.code_artist_number === undefined)
10099
) {
101-
res.status(400);
102-
res.send(
103-
'Missing query parameter. Query must include: artist_name, album_title, or code_letters, code_artist_number, and code_number'
104-
);
100+
res.status(400).json({
101+
message:
102+
'Missing query parameter. Query must include: artist_name, album_title, or code_letters, code_artist_number, and code_number',
103+
});
105104
} else if (query.code_letters !== undefined && query.code_artist_number !== undefined) {
106105
//quickly look up albums by that artist
107-
res.status(501);
108-
res.send('TODO: Library Code Lookup');
106+
res.status(501).json({ message: 'TODO: Library Code Lookup' });
109107
} else {
110108
try {
111109
const response = await libraryService.fuzzySearchLibrary(query.artist_name, query.album_title, query.n);
@@ -128,8 +126,7 @@ export const addArtist: RequestHandler = async (req: Request<object, object, New
128126
const { body } = req;
129127
//TODO auto_generate artist code letters and make it an optional parameter
130128
if (body.artist_name === undefined || body.code_letters === undefined || body.genre_id === undefined) {
131-
res.status(400);
132-
res.send('Missing Request Parameters: artist_name, code_letters, or genre_id');
129+
res.status(400).json({ message: 'Missing Request Parameters: artist_name, code_letters, or genre_id' });
133130
} else {
134131
try {
135132
const generatedArtistNumber = await libraryService.generateArtistNumber(body.code_letters, body.genre_id);
@@ -166,7 +163,7 @@ export const getRotation: RequestHandler = async (req, res, next) => {
166163
export type RotationAddRequest = Omit<NewRotationRelease, 'id'>;
167164
export const addRotation: RequestHandler<object, unknown, NewRotationRelease> = async (req, res, next) => {
168165
if (req.body.album_id === undefined || req.body.play_freq === undefined) {
169-
res.status(400).send('Missing Parameters: album_id or play_freq');
166+
res.status(400).json({ message: 'Missing Parameters: album_id or play_freq' });
170167
} else {
171168
try {
172169
const rotationRelease: RotationRelease = await libraryService.addToRotation(req.body);
@@ -187,16 +184,16 @@ export const killRotation: RequestHandler<object, unknown, KillRotationRelease>
187184
const { body } = req;
188185

189186
if (body.rotation_id === undefined) {
190-
res.status(400).send('Bad Request, Missing Parameter: rotation_id');
187+
res.status(400).json({ message: 'Bad Request, Missing Parameter: rotation_id' });
191188
} else if (body.kill_date !== undefined && !libraryService.isISODate(body.kill_date)) {
192-
res.status(400).send('Bad Request, Incorrect Date Format: kill_date should be of form YYYY-MM-DD');
189+
res.status(400).json({ message: 'Bad Request, Incorrect Date Format: kill_date should be of form YYYY-MM-DD' });
193190
} else {
194191
try {
195192
const updatedRotation: RotationRelease = await libraryService.killRotationInDB(body.rotation_id, body.kill_date);
196193
if (updatedRotation !== undefined) {
197194
res.status(200).json(updatedRotation);
198195
} else {
199-
res.status(400).json({ status: 400, message: 'Rotation entry not found' });
196+
res.status(400).json({ message: 'Rotation entry not found' });
200197
}
201198
} catch (e) {
202199
console.error('Failed to update rotation kill_date');
@@ -220,7 +217,7 @@ export const getFormats: RequestHandler = async (req, res, next) => {
220217
export const addFormat: RequestHandler = async (req, res, next) => {
221218
const { body } = req;
222219
if (body.name === undefined) {
223-
res.status(400).send('Bad Request, Missing Parameter: name');
220+
res.status(400).json({ message: 'Bad Request, Missing Parameter: name' });
224221
} else {
225222
try {
226223
const newFormat: NewAlbumFormat = {
@@ -245,7 +242,7 @@ export const getGenres: RequestHandler = async (req, res) => {
245242
export const addGenre: RequestHandler = async (req, res, next) => {
246243
const { body } = req;
247244
if (body.name === undefined || body.description === undefined) {
248-
res.status(400).send('Bad Request, Parameters name and description are required.');
245+
res.status(400).json({ message: 'Bad Request, Parameters name and description are required.' });
249246
} else {
250247
try {
251248
const newGenre: NewGenre = {
@@ -271,7 +268,7 @@ export const getAlbum: RequestHandler<object, unknown, unknown, { album_id: stri
271268
const { query } = req;
272269
if (query.album_id === undefined) {
273270
console.error('Bad Request, missing album identifier: album_id');
274-
res.status(400).send('Bad Request, missing album identifier: album_id');
271+
res.status(400).json({ message: 'Bad Request, missing album identifier: album_id' });
275272
} else {
276273
try {
277274
const album = await libraryService.getAlbumFromDB(parseInt(query.album_id));

apps/backend/middleware/errorHandler.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ function errorHandler(err: any, req: Request, res: Response, next: NextFunction)
1010
if (error instanceof WxycError) {
1111
res.status(error.statusCode).json({ message: error.message });
1212
} else {
13-
res.status(500).json({ message: error.message });
13+
console.error('Unhandled error:', error);
14+
res.status(500).json({ message: 'Internal server error' });
1415
}
1516
}
1617

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import errorHandler from '../../../apps/backend/middleware/errorHandler';
2+
import WxycError from '../../../apps/backend/utils/error';
3+
import { Request, Response, NextFunction } from 'express';
4+
5+
function mockResponse(): Response {
6+
const res = {
7+
status: jest.fn().mockReturnThis(),
8+
json: jest.fn().mockReturnThis(),
9+
};
10+
return res as unknown as Response;
11+
}
12+
13+
const mockReq = {} as Request;
14+
const mockNext = jest.fn() as NextFunction;
15+
16+
describe('errorHandler middleware', () => {
17+
it('returns { message } with correct status for WxycError', () => {
18+
const res = mockResponse();
19+
const error = new WxycError('Album not found', 404);
20+
21+
errorHandler(error, mockReq, res, mockNext);
22+
23+
expect(res.status).toHaveBeenCalledWith(404);
24+
expect(res.json).toHaveBeenCalledWith({ message: 'Album not found' });
25+
});
26+
27+
it('returns generic message for non-WxycError (does not leak internals)', () => {
28+
const res = mockResponse();
29+
const error = new Error('SELECT * FROM users failed: connection refused');
30+
31+
errorHandler(error, mockReq, res, mockNext);
32+
33+
expect(res.status).toHaveBeenCalledWith(500);
34+
expect(res.json).toHaveBeenCalledWith({ message: 'Internal server error' });
35+
});
36+
37+
it('handles non-Error values thrown', () => {
38+
const res = mockResponse();
39+
40+
errorHandler('something broke', mockReq, res, mockNext);
41+
42+
expect(res.status).toHaveBeenCalledWith(500);
43+
expect(res.json).toHaveBeenCalledWith({ message: 'Internal server error' });
44+
});
45+
46+
it('logs non-WxycError errors to console', () => {
47+
const res = mockResponse();
48+
const error = new Error('db connection lost');
49+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
50+
51+
errorHandler(error, mockReq, res, mockNext);
52+
53+
expect(consoleSpy).toHaveBeenCalledWith('Unhandled error:', error);
54+
consoleSpy.mockRestore();
55+
});
56+
});

0 commit comments

Comments
 (0)