Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions spec/MongoStorageAdapter.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
it('should not pass metadata to MongoClient by default', async () => {
const adapter = new MongoStorageAdapter({ uri: databaseURI });
Expand Down
2 changes: 1 addition & 1 deletion spec/ParseServer.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ describe('Server Url Checks', () => {
parseServerProcess.on('close', async code => {
expect(code).toEqual(1);
expect(stdout).not.toContain('UnhandledPromiseRejectionWarning');
expect(stderr).toContain('MongoServerSelectionError');
expect(stderr).toContain('Database error');
await reconfigureServer();
done();
});
Expand Down
2 changes: 1 addition & 1 deletion spec/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ describe('server', () => {
}),
});
const error = await server.start().catch(e => e);
expect(`${error}`.includes('MongoServerSelectionError')).toBeTrue();
expect(`${error}`.includes('Database error')).toBeTrue();
await reconfigureServer();
});

Expand Down
37 changes: 37 additions & 0 deletions src/Adapters/Storage/Mongo/MongoStorageAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,36 @@ const ReadPreference = mongodb.ReadPreference;

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 => {
return mongoAdapter
.connect()
Expand Down Expand Up @@ -252,6 +282,13 @@ export class MongoStorageAdapter implements StorageAdapter {
delete this.connectionPromise;
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;
}

Expand Down
4 changes: 2 additions & 2 deletions src/middlewares.js
Original file line number Diff line number Diff line change
Expand Up @@ -378,9 +378,9 @@ export const handleParseSession = async (req, res, next) => {
next(error);
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);
throw new Parse.Error(Parse.Error.UNKNOWN_ERROR, error);
next(new Parse.Error(Parse.Error.UNKNOWN_ERROR, 'Unknown error'));
}
};

Expand Down
Loading