Skip to content

Commit 1d3336d

Browse files
authored
fix: MongoDB timeout errors unhandled and potentially revealing internal data (#10020)
1 parent 1b5bd2f commit 1d3336d

File tree

5 files changed

+164
-4
lines changed

5 files changed

+164
-4
lines changed

spec/MongoStorageAdapter.spec.js

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,6 +1064,129 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
10641064
});
10651065
});
10661066

1067+
describe('transient error handling', () => {
1068+
it('should transform MongoWaitQueueTimeoutError to Parse.Error.INTERNAL_SERVER_ERROR', async () => {
1069+
const adapter = new MongoStorageAdapter({ uri: databaseURI });
1070+
await adapter.connect();
1071+
1072+
// Create a mock error with the MongoWaitQueueTimeoutError name
1073+
const mockError = new Error('Timed out while checking out a connection from connection pool');
1074+
mockError.name = 'MongoWaitQueueTimeoutError';
1075+
1076+
try {
1077+
adapter.handleError(mockError);
1078+
fail('Expected handleError to throw');
1079+
} catch (error) {
1080+
expect(error instanceof Parse.Error).toBe(true);
1081+
expect(error.code).toBe(Parse.Error.INTERNAL_SERVER_ERROR);
1082+
expect(error.message).toBe('Database error');
1083+
}
1084+
});
1085+
1086+
it('should transform MongoServerSelectionError to Parse.Error.INTERNAL_SERVER_ERROR', async () => {
1087+
const adapter = new MongoStorageAdapter({ uri: databaseURI });
1088+
await adapter.connect();
1089+
1090+
const mockError = new Error('Server selection timed out');
1091+
mockError.name = 'MongoServerSelectionError';
1092+
1093+
try {
1094+
adapter.handleError(mockError);
1095+
fail('Expected handleError to throw');
1096+
} catch (error) {
1097+
expect(error instanceof Parse.Error).toBe(true);
1098+
expect(error.code).toBe(Parse.Error.INTERNAL_SERVER_ERROR);
1099+
expect(error.message).toBe('Database error');
1100+
}
1101+
});
1102+
1103+
it('should transform MongoNetworkTimeoutError to Parse.Error.INTERNAL_SERVER_ERROR', async () => {
1104+
const adapter = new MongoStorageAdapter({ uri: databaseURI });
1105+
await adapter.connect();
1106+
1107+
const mockError = new Error('Network timeout');
1108+
mockError.name = 'MongoNetworkTimeoutError';
1109+
1110+
try {
1111+
adapter.handleError(mockError);
1112+
fail('Expected handleError to throw');
1113+
} catch (error) {
1114+
expect(error instanceof Parse.Error).toBe(true);
1115+
expect(error.code).toBe(Parse.Error.INTERNAL_SERVER_ERROR);
1116+
expect(error.message).toBe('Database error');
1117+
}
1118+
});
1119+
1120+
it('should transform MongoNetworkError to Parse.Error.INTERNAL_SERVER_ERROR', async () => {
1121+
const adapter = new MongoStorageAdapter({ uri: databaseURI });
1122+
await adapter.connect();
1123+
1124+
const mockError = new Error('Network error');
1125+
mockError.name = 'MongoNetworkError';
1126+
1127+
try {
1128+
adapter.handleError(mockError);
1129+
fail('Expected handleError to throw');
1130+
} catch (error) {
1131+
expect(error instanceof Parse.Error).toBe(true);
1132+
expect(error.code).toBe(Parse.Error.INTERNAL_SERVER_ERROR);
1133+
expect(error.message).toBe('Database error');
1134+
}
1135+
});
1136+
1137+
it('should transform TransientTransactionError to Parse.Error.INTERNAL_SERVER_ERROR', async () => {
1138+
const adapter = new MongoStorageAdapter({ uri: databaseURI });
1139+
await adapter.connect();
1140+
1141+
const mockError = new Error('Transient transaction error');
1142+
mockError.hasErrorLabel = label => label === 'TransientTransactionError';
1143+
1144+
try {
1145+
adapter.handleError(mockError);
1146+
fail('Expected handleError to throw');
1147+
} catch (error) {
1148+
expect(error instanceof Parse.Error).toBe(true);
1149+
expect(error.code).toBe(Parse.Error.INTERNAL_SERVER_ERROR);
1150+
expect(error.message).toBe('Database error');
1151+
}
1152+
});
1153+
1154+
it('should not transform non-transient errors', async () => {
1155+
const adapter = new MongoStorageAdapter({ uri: databaseURI });
1156+
await adapter.connect();
1157+
1158+
const mockError = new Error('Some other error');
1159+
mockError.name = 'SomeOtherError';
1160+
1161+
try {
1162+
adapter.handleError(mockError);
1163+
fail('Expected handleError to throw');
1164+
} catch (error) {
1165+
expect(error instanceof Parse.Error).toBe(false);
1166+
expect(error.message).toBe('Some other error');
1167+
}
1168+
});
1169+
1170+
it('should handle null/undefined errors', async () => {
1171+
const adapter = new MongoStorageAdapter({ uri: databaseURI });
1172+
await adapter.connect();
1173+
1174+
try {
1175+
adapter.handleError(null);
1176+
fail('Expected handleError to throw');
1177+
} catch (error) {
1178+
expect(error).toBeNull();
1179+
}
1180+
1181+
try {
1182+
adapter.handleError(undefined);
1183+
fail('Expected handleError to throw');
1184+
} catch (error) {
1185+
expect(error).toBeUndefined();
1186+
}
1187+
});
1188+
});
1189+
10671190
describe('MongoDB Client Metadata', () => {
10681191
it('should not pass metadata to MongoClient by default', async () => {
10691192
const adapter = new MongoStorageAdapter({ uri: databaseURI });

spec/ParseServer.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ describe('Server Url Checks', () => {
5555
parseServerProcess.on('close', async code => {
5656
expect(code).toEqual(1);
5757
expect(stdout).not.toContain('UnhandledPromiseRejectionWarning');
58-
expect(stderr).toContain('MongoServerSelectionError');
58+
expect(stderr).toContain('Database error');
5959
await reconfigureServer();
6060
done();
6161
});

spec/index.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ describe('server', () => {
7373
}),
7474
});
7575
const error = await server.start().catch(e => e);
76-
expect(`${error}`.includes('MongoServerSelectionError')).toBeTrue();
76+
expect(`${error}`.includes('Database error')).toBeTrue();
7777
await reconfigureServer();
7878
});
7979

src/Adapters/Storage/Mongo/MongoStorageAdapter.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,36 @@ const ReadPreference = mongodb.ReadPreference;
2727

2828
const MongoSchemaCollectionName = '_SCHEMA';
2929

30+
/**
31+
* Determines if a MongoDB error is a transient infrastructure error
32+
* (connection pool, network, server selection) as opposed to a query-level error.
33+
*/
34+
function isTransientError(error) {
35+
if (!error) {
36+
return false;
37+
}
38+
39+
// Connection pool, network, and server selection errors
40+
const transientErrorNames = [
41+
'MongoWaitQueueTimeoutError',
42+
'MongoServerSelectionError',
43+
'MongoNetworkTimeoutError',
44+
'MongoNetworkError',
45+
];
46+
if (transientErrorNames.includes(error.name)) {
47+
return true;
48+
}
49+
50+
// Check for MongoDB's transient transaction error label
51+
if (typeof error.hasErrorLabel === 'function') {
52+
if (error.hasErrorLabel('TransientTransactionError')) {
53+
return true;
54+
}
55+
}
56+
57+
return false;
58+
}
59+
3060
const storageAdapterAllCollections = mongoAdapter => {
3161
return mongoAdapter
3262
.connect()
@@ -252,6 +282,13 @@ export class MongoStorageAdapter implements StorageAdapter {
252282
delete this.connectionPromise;
253283
logger.error('Received unauthorized error', { error: error });
254284
}
285+
286+
// Transform infrastructure/transient errors into Parse.Error.INTERNAL_SERVER_ERROR
287+
if (isTransientError(error)) {
288+
logger.error('Database transient error', error);
289+
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Database error');
290+
}
291+
255292
throw error;
256293
}
257294

src/middlewares.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,9 +378,9 @@ export const handleParseSession = async (req, res, next) => {
378378
next(error);
379379
return;
380380
}
381-
// TODO: Determine the correct error scenario.
381+
// Log full error details internally, but don't expose to client
382382
req.config.loggerController.error('error getting auth for sessionToken', error);
383-
throw new Parse.Error(Parse.Error.UNKNOWN_ERROR, error);
383+
next(new Parse.Error(Parse.Error.UNKNOWN_ERROR, 'Unknown error'));
384384
}
385385
};
386386

0 commit comments

Comments
 (0)