From f674c8bf879f1dedf0ec31ee2ab6b9679f74befe Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 5 Jul 2026 01:55:06 +1000 Subject: [PATCH] fix: Server fails to start with IndexKeySpecsConflict on _Idempotency TTL index on MongoDB 8.0 --- spec/MongoStorageAdapter.spec.js | 28 +++++++++++++++++++ .../Storage/Mongo/MongoStorageAdapter.js | 17 ++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/spec/MongoStorageAdapter.spec.js b/spec/MongoStorageAdapter.spec.js index facf1b2c41..daf9596b50 100644 --- a/spec/MongoStorageAdapter.spec.js +++ b/spec/MongoStorageAdapter.spec.js @@ -5,6 +5,7 @@ const { MongoClient, Collection } = require('mongodb'); const databaseURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase'; const request = require('../lib/request'); const Config = require('../lib/Config'); +const SchemaController = require('../lib/Controllers/SchemaController'); const TestUtils = require('../lib/TestUtils'); const Utils = require('../lib/Utils'); const { randomUUID: uuidv4 } = require('crypto'); @@ -900,6 +901,33 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { expect(userIndexes.find(idx => idx.name === '_perishable_token' || idx.name === '_perishable_token_1')).toBeDefined(); expect(roleIndexes.find(idx => idx.name === 'name_1')).toBeDefined(); }); + + it('ensureIndex tolerates an existing conflicting index instead of crashing startup (#10431)', async () => { + await reconfigureServer({ databaseAdapter: undefined, databaseURI, databaseOptions: {} }); + const adapter = Config.get(Parse.applicationId).database.adapter; + const collection = await adapter._adaptiveCollection('_Idempotency'); + // Simulate a pre-existing `ttl` index created by an older Parse Server / driver whose stored + // spec conflicts with the one requested on startup (same name + key, different options). This + // produces IndexKeySpecsConflict (code 86) — the same failure MongoDB 8.0 triggers via its + // internal `enableOrderedIndex` field, which previously aborted server startup. + await collection._mongoCollection.dropIndex('ttl').catch(() => {}); + await collection._mongoCollection.createIndex( + { expire: 1 }, + { name: 'ttl', expireAfterSeconds: 100, sparse: true } + ); + const schema = { + fields: { + ...SchemaController.defaultColumns._Default, + ...SchemaController.defaultColumns._Idempotency, + }, + }; + // Mirrors DatabaseController.performInitialization's TTL-index call, which previously threw. + await expectAsync( + adapter.ensureIndex('_Idempotency', schema, ['expire'], 'ttl', false, { ttl: 0 }) + ).toBeResolved(); + const indexes = await getIndexes('_Idempotency'); + expect(indexes.find(idx => idx.name === 'ttl')).toBeDefined(); + }); }); describe('logClientEvents', () => { diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 9c17b2a18b..4bd83c57f8 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -818,7 +818,22 @@ export class MongoStorageAdapter implements StorageAdapter { .then(collection => collection._mongoCollection.createIndex(indexCreationRequest, indexOptions) ) - .catch(err => this.handleError(err)); + .catch(error => { + // MongoDB 8.0 enriches stored index specs with internal fields (e.g. `enableOrderedIndex`) + // that Parse Server never sets. Re-issuing an otherwise-identical createIndex on startup + // then fails with IndexKeySpecsConflict (86) / IndexOptionsConflict (85) even though a + // functionally-equivalent index already exists — which aborts server startup. Treat that + // as success (the existing index already serves its purpose), mirroring + // ensureAuthDataUniqueness. Logged so a genuine conflict is still visible. (#10431) + if (error.code === 85 || error.code === 86) { + logger.warn( + `Index "${indexName || ''}" on ${className} already exists with a conflicting ` + + `specification; keeping the existing index. (code ${error.code})` + ); + return; + } + return this.handleError(error); + }); } // Create a unique index. Unique indexes on nullable fields are not allowed. Since we don't