fix: MongoDB timeout errors unhandled and potentially revealing internal data (#10020)
This commit is contained in:
@@ -1064,6 +1064,129 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('transient error handling', () => {
|
||||||
|
it('should transform MongoWaitQueueTimeoutError to Parse.Error.INTERNAL_SERVER_ERROR', async () => {
|
||||||
|
const adapter = new MongoStorageAdapter({ uri: databaseURI });
|
||||||
|
await adapter.connect();
|
||||||
|
|
||||||
|
// Create a mock error with the MongoWaitQueueTimeoutError name
|
||||||
|
const mockError = new Error('Timed out while checking out a connection from connection pool');
|
||||||
|
mockError.name = 'MongoWaitQueueTimeoutError';
|
||||||
|
|
||||||
|
try {
|
||||||
|
adapter.handleError(mockError);
|
||||||
|
fail('Expected handleError to throw');
|
||||||
|
} catch (error) {
|
||||||
|
expect(error instanceof Parse.Error).toBe(true);
|
||||||
|
expect(error.code).toBe(Parse.Error.INTERNAL_SERVER_ERROR);
|
||||||
|
expect(error.message).toBe('Database error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transform MongoServerSelectionError to Parse.Error.INTERNAL_SERVER_ERROR', async () => {
|
||||||
|
const adapter = new MongoStorageAdapter({ uri: databaseURI });
|
||||||
|
await adapter.connect();
|
||||||
|
|
||||||
|
const mockError = new Error('Server selection timed out');
|
||||||
|
mockError.name = 'MongoServerSelectionError';
|
||||||
|
|
||||||
|
try {
|
||||||
|
adapter.handleError(mockError);
|
||||||
|
fail('Expected handleError to throw');
|
||||||
|
} catch (error) {
|
||||||
|
expect(error instanceof Parse.Error).toBe(true);
|
||||||
|
expect(error.code).toBe(Parse.Error.INTERNAL_SERVER_ERROR);
|
||||||
|
expect(error.message).toBe('Database error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transform MongoNetworkTimeoutError to Parse.Error.INTERNAL_SERVER_ERROR', async () => {
|
||||||
|
const adapter = new MongoStorageAdapter({ uri: databaseURI });
|
||||||
|
await adapter.connect();
|
||||||
|
|
||||||
|
const mockError = new Error('Network timeout');
|
||||||
|
mockError.name = 'MongoNetworkTimeoutError';
|
||||||
|
|
||||||
|
try {
|
||||||
|
adapter.handleError(mockError);
|
||||||
|
fail('Expected handleError to throw');
|
||||||
|
} catch (error) {
|
||||||
|
expect(error instanceof Parse.Error).toBe(true);
|
||||||
|
expect(error.code).toBe(Parse.Error.INTERNAL_SERVER_ERROR);
|
||||||
|
expect(error.message).toBe('Database error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transform MongoNetworkError to Parse.Error.INTERNAL_SERVER_ERROR', async () => {
|
||||||
|
const adapter = new MongoStorageAdapter({ uri: databaseURI });
|
||||||
|
await adapter.connect();
|
||||||
|
|
||||||
|
const mockError = new Error('Network error');
|
||||||
|
mockError.name = 'MongoNetworkError';
|
||||||
|
|
||||||
|
try {
|
||||||
|
adapter.handleError(mockError);
|
||||||
|
fail('Expected handleError to throw');
|
||||||
|
} catch (error) {
|
||||||
|
expect(error instanceof Parse.Error).toBe(true);
|
||||||
|
expect(error.code).toBe(Parse.Error.INTERNAL_SERVER_ERROR);
|
||||||
|
expect(error.message).toBe('Database error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transform TransientTransactionError to Parse.Error.INTERNAL_SERVER_ERROR', async () => {
|
||||||
|
const adapter = new MongoStorageAdapter({ uri: databaseURI });
|
||||||
|
await adapter.connect();
|
||||||
|
|
||||||
|
const mockError = new Error('Transient transaction error');
|
||||||
|
mockError.hasErrorLabel = label => label === 'TransientTransactionError';
|
||||||
|
|
||||||
|
try {
|
||||||
|
adapter.handleError(mockError);
|
||||||
|
fail('Expected handleError to throw');
|
||||||
|
} catch (error) {
|
||||||
|
expect(error instanceof Parse.Error).toBe(true);
|
||||||
|
expect(error.code).toBe(Parse.Error.INTERNAL_SERVER_ERROR);
|
||||||
|
expect(error.message).toBe('Database error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not transform non-transient errors', async () => {
|
||||||
|
const adapter = new MongoStorageAdapter({ uri: databaseURI });
|
||||||
|
await adapter.connect();
|
||||||
|
|
||||||
|
const mockError = new Error('Some other error');
|
||||||
|
mockError.name = 'SomeOtherError';
|
||||||
|
|
||||||
|
try {
|
||||||
|
adapter.handleError(mockError);
|
||||||
|
fail('Expected handleError to throw');
|
||||||
|
} catch (error) {
|
||||||
|
expect(error instanceof Parse.Error).toBe(false);
|
||||||
|
expect(error.message).toBe('Some other error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle null/undefined errors', async () => {
|
||||||
|
const adapter = new MongoStorageAdapter({ uri: databaseURI });
|
||||||
|
await adapter.connect();
|
||||||
|
|
||||||
|
try {
|
||||||
|
adapter.handleError(null);
|
||||||
|
fail('Expected handleError to throw');
|
||||||
|
} catch (error) {
|
||||||
|
expect(error).toBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
adapter.handleError(undefined);
|
||||||
|
fail('Expected handleError to throw');
|
||||||
|
} catch (error) {
|
||||||
|
expect(error).toBeUndefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('MongoDB Client Metadata', () => {
|
describe('MongoDB Client Metadata', () => {
|
||||||
it('should not pass metadata to MongoClient by default', async () => {
|
it('should not pass metadata to MongoClient by default', async () => {
|
||||||
const adapter = new MongoStorageAdapter({ uri: databaseURI });
|
const adapter = new MongoStorageAdapter({ uri: databaseURI });
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ describe('Server Url Checks', () => {
|
|||||||
parseServerProcess.on('close', async code => {
|
parseServerProcess.on('close', async code => {
|
||||||
expect(code).toEqual(1);
|
expect(code).toEqual(1);
|
||||||
expect(stdout).not.toContain('UnhandledPromiseRejectionWarning');
|
expect(stdout).not.toContain('UnhandledPromiseRejectionWarning');
|
||||||
expect(stderr).toContain('MongoServerSelectionError');
|
expect(stderr).toContain('Database error');
|
||||||
await reconfigureServer();
|
await reconfigureServer();
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ describe('server', () => {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const error = await server.start().catch(e => e);
|
const error = await server.start().catch(e => e);
|
||||||
expect(`${error}`.includes('MongoServerSelectionError')).toBeTrue();
|
expect(`${error}`.includes('Database error')).toBeTrue();
|
||||||
await reconfigureServer();
|
await reconfigureServer();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,36 @@ const ReadPreference = mongodb.ReadPreference;
|
|||||||
|
|
||||||
const MongoSchemaCollectionName = '_SCHEMA';
|
const MongoSchemaCollectionName = '_SCHEMA';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if a MongoDB error is a transient infrastructure error
|
||||||
|
* (connection pool, network, server selection) as opposed to a query-level error.
|
||||||
|
*/
|
||||||
|
function isTransientError(error) {
|
||||||
|
if (!error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection pool, network, and server selection errors
|
||||||
|
const transientErrorNames = [
|
||||||
|
'MongoWaitQueueTimeoutError',
|
||||||
|
'MongoServerSelectionError',
|
||||||
|
'MongoNetworkTimeoutError',
|
||||||
|
'MongoNetworkError',
|
||||||
|
];
|
||||||
|
if (transientErrorNames.includes(error.name)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for MongoDB's transient transaction error label
|
||||||
|
if (typeof error.hasErrorLabel === 'function') {
|
||||||
|
if (error.hasErrorLabel('TransientTransactionError')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const storageAdapterAllCollections = mongoAdapter => {
|
const storageAdapterAllCollections = mongoAdapter => {
|
||||||
return mongoAdapter
|
return mongoAdapter
|
||||||
.connect()
|
.connect()
|
||||||
@@ -252,6 +282,13 @@ export class MongoStorageAdapter implements StorageAdapter {
|
|||||||
delete this.connectionPromise;
|
delete this.connectionPromise;
|
||||||
logger.error('Received unauthorized error', { error: error });
|
logger.error('Received unauthorized error', { error: error });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Transform infrastructure/transient errors into Parse.Error.INTERNAL_SERVER_ERROR
|
||||||
|
if (isTransientError(error)) {
|
||||||
|
logger.error('Database transient error', error);
|
||||||
|
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Database error');
|
||||||
|
}
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -378,9 +378,9 @@ export const handleParseSession = async (req, res, next) => {
|
|||||||
next(error);
|
next(error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// TODO: Determine the correct error scenario.
|
// Log full error details internally, but don't expose to client
|
||||||
req.config.loggerController.error('error getting auth for sessionToken', error);
|
req.config.loggerController.error('error getting auth for sessionToken', error);
|
||||||
throw new Parse.Error(Parse.Error.UNKNOWN_ERROR, error);
|
next(new Parse.Error(Parse.Error.UNKNOWN_ERROR, 'Unknown error'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user