diff --git a/src/index.ts b/src/index.ts index 5c26ae1b70..9f4574c251 100644 --- a/src/index.ts +++ b/src/index.ts @@ -543,7 +543,12 @@ export type { export type { InsertManyResult, InsertOneOptions, InsertOneResult } from './operations/insert'; export type { CollectionInfo, ListCollectionsOptions } from './operations/list_collections'; export type { ListDatabasesOptions, ListDatabasesResult } from './operations/list_databases'; -export type { AbstractOperation, Hint, OperationOptions } from './operations/operation'; +export type { + AbstractOperation, + Hint, + ModernizedOperation, + OperationOptions +} from './operations/operation'; export type { ProfilingLevelOptions } from './operations/profiling_level'; export type { RemoveUserOptions } from './operations/remove_user'; export type { RenameOptions } from './operations/rename'; diff --git a/src/operations/command.ts b/src/operations/command.ts index 14d3762997..36216fbd98 100644 --- a/src/operations/command.ts +++ b/src/operations/command.ts @@ -1,3 +1,4 @@ +import { type Connection } from '..'; import type { BSONSerializeOptions, Document } from '../bson'; import { type MongoDBResponseConstructor } from '../cmap/wire_protocol/responses'; import { MongoInvalidArgumentError } from '../error'; @@ -9,14 +10,14 @@ import { } from '../explain'; import { ReadConcern } from '../read_concern'; import type { ReadPreference } from '../read_preference'; -import type { Server } from '../sdam/server'; +import type { Server, ServerCommandOptions } from '../sdam/server'; import { MIN_SECONDARY_WRITE_WIRE_VERSION } from '../sdam/server_selection'; import type { ClientSession } from '../sessions'; import { type TimeoutContext } from '../timeout'; import { commandSupportsReadConcern, maxWireVersion, MongoDBNamespace } from '../utils'; import { WriteConcern, type WriteConcernOptions } from '../write_concern'; import type { ReadConcernLike } from './../read_concern'; -import { AbstractOperation, Aspect, type OperationOptions } from './operation'; +import { AbstractOperation, Aspect, ModernizedOperation, type OperationOptions } from './operation'; /** @public */ export interface CollationOptions { @@ -183,3 +184,94 @@ export abstract class CommandOperation extends AbstractOperation { return await server.command(this.ns, cmd, options, responseType); } } + +/** @internal */ +export abstract class ModernizedCommandOperation extends ModernizedOperation { + override options: CommandOperationOptions; + readConcern?: ReadConcern; + writeConcern?: WriteConcern; + explain?: Explain; + + constructor(parent?: OperationParent, options?: CommandOperationOptions) { + super(options); + this.options = options ?? {}; + + // NOTE: this was explicitly added for the add/remove user operations, it's likely + // something we'd want to reconsider. Perhaps those commands can use `Admin` + // as a parent? + const dbNameOverride = options?.dbName || options?.authdb; + if (dbNameOverride) { + this.ns = new MongoDBNamespace(dbNameOverride, '$cmd'); + } else { + this.ns = parent + ? parent.s.namespace.withCollection('$cmd') + : new MongoDBNamespace('admin', '$cmd'); + } + + this.readConcern = ReadConcern.fromOptions(options); + this.writeConcern = WriteConcern.fromOptions(options); + + if (this.hasAspect(Aspect.EXPLAINABLE)) { + this.explain = Explain.fromOptions(options); + if (this.explain) validateExplainTimeoutOptions(this.options, this.explain); + } else if (options?.explain != null) { + throw new MongoInvalidArgumentError(`Option "explain" is not supported on this command`); + } + } + + override get canRetryWrite(): boolean { + if (this.hasAspect(Aspect.EXPLAINABLE)) { + return this.explain == null; + } + return super.canRetryWrite; + } + + abstract buildCommandDocument(connection: Connection, session?: ClientSession): Document; + + override buildOptions(timeoutContext: TimeoutContext): ServerCommandOptions { + return { + ...this.options, + ...this.bsonOptions, + timeoutContext, + readPreference: this.readPreference, + session: this.session + }; + } + + override buildCommand(connection: Connection, session?: ClientSession): Document { + const command = this.buildCommandDocument(connection, session); + + const serverWireVersion = maxWireVersion(connection); + const inTransaction = this.session && this.session.inTransaction(); + + if (this.readConcern && commandSupportsReadConcern(command) && !inTransaction) { + Object.assign(command, { readConcern: this.readConcern }); + } + + if (this.trySecondaryWrite && serverWireVersion < MIN_SECONDARY_WRITE_WIRE_VERSION) { + command.omitReadPreference = true; + } + + if (this.writeConcern && this.hasAspect(Aspect.WRITE_OPERATION) && !inTransaction) { + WriteConcern.apply(command, this.writeConcern); + } + + if ( + this.options.collation && + typeof this.options.collation === 'object' && + !this.hasAspect(Aspect.SKIP_COLLATION) + ) { + Object.assign(command, { collation: this.options.collation }); + } + + if (typeof this.options.maxTimeMS === 'number') { + command.maxTimeMS = this.options.maxTimeMS; + } + + if (this.hasAspect(Aspect.EXPLAINABLE) && this.explain) { + return decorateWithExplain(command, this.explain); + } + + return command; + } +} diff --git a/src/operations/drop.ts b/src/operations/drop.ts index 3fd4ac6dac..833321fe01 100644 --- a/src/operations/drop.ts +++ b/src/operations/drop.ts @@ -1,7 +1,8 @@ +import { MongoServerError } from '..'; import type { Document } from '../bson'; import { CursorTimeoutContext } from '../cursor/abstract_cursor'; import type { Db } from '../db'; -import { MONGODB_ERROR_CODES, MongoServerError } from '../error'; +import { MONGODB_ERROR_CODES } from '../error'; import type { Server } from '../sdam/server'; import type { ClientSession } from '../sessions'; import { TimeoutContext } from '../timeout'; diff --git a/src/operations/execute_operation.ts b/src/operations/execute_operation.ts index 30d57fdb46..5306005a0d 100644 --- a/src/operations/execute_operation.ts +++ b/src/operations/execute_operation.ts @@ -26,7 +26,7 @@ import type { Topology } from '../sdam/topology'; import type { ClientSession } from '../sessions'; import { TimeoutContext } from '../timeout'; import { abortable, supportsRetryableWrites } from '../utils'; -import { AbstractOperation, Aspect } from './operation'; +import { AbstractOperation, Aspect, ModernizedOperation } from './operation'; const MMAPv1_RETRY_WRITES_ERROR_CODE = MONGODB_ERROR_CODES.IllegalOperation; const MMAPv1_RETRY_WRITES_ERROR_MESSAGE = @@ -85,6 +85,8 @@ export async function executeOperation< throw new MongoInvalidArgumentError('ClientSession must be from the same MongoClient'); } + operation.session ??= session; + const readPreference = operation.readPreference ?? ReadPreference.primary; const inTransaction = !!session?.inTransaction(); @@ -231,6 +233,8 @@ async function tryOperation< let previousOperationError: MongoError | undefined; let previousServer: ServerDescription | undefined; + const isModernOperation = operation instanceof ModernizedOperation; + for (let tries = 0; tries < maxTries; tries++) { if (previousOperationError) { if (hasWriteAspect && previousOperationError.code === MMAPv1_RETRY_WRITES_ERROR_CODE) { @@ -276,12 +280,24 @@ async function tryOperation< } } + operation.server = server; + try { // If tries > 0 and we are command batching we need to reset the batch. if (tries > 0 && operation.hasAspect(Aspect.COMMAND_BATCHING)) { operation.resetBatch(); } - return await operation.execute(server, session, timeoutContext); + + if (!isModernOperation) { + return await operation.execute(server, session, timeoutContext); + } + + try { + const result = await server.modernCommand(operation, timeoutContext); + return operation.handleOk(result); + } catch (error) { + return operation.handleError(error); + } } catch (operationError) { if (!(operationError instanceof MongoError)) throw operationError; if ( diff --git a/src/operations/insert.ts b/src/operations/insert.ts index 588468f313..bbc324e65d 100644 --- a/src/operations/insert.ts +++ b/src/operations/insert.ts @@ -1,5 +1,7 @@ +import { type Connection } from '..'; import type { Document } from '../bson'; import type { BulkWriteOptions } from '../bulk/common'; +import { MongoDBResponse } from '../cmap/wire_protocol/responses'; import type { Collection } from '../collection'; import { MongoServerError } from '../error'; import type { InferIdType } from '../mongo_types'; @@ -7,12 +9,13 @@ import type { Server } from '../sdam/server'; import type { ClientSession } from '../sessions'; import { type TimeoutContext } from '../timeout'; import { maybeAddIdToDocuments, type MongoDBNamespace } from '../utils'; -import { CommandOperation, type CommandOperationOptions } from './command'; +import { type CommandOperationOptions, ModernizedCommandOperation } from './command'; import { Aspect, defineAspects } from './operation'; - /** @internal */ -export class InsertOperation extends CommandOperation { +export class InsertOperation extends ModernizedCommandOperation { + override SERVER_COMMAND_RESPONSE_TYPE = MongoDBResponse; override options: BulkWriteOptions; + documents: Document[]; constructor(ns: MongoDBNamespace, documents: Document[], options: BulkWriteOptions) { @@ -26,11 +29,7 @@ export class InsertOperation extends CommandOperation { return 'insert' as const; } - override async execute( - server: Server, - session: ClientSession | undefined, - timeoutContext: TimeoutContext - ): Promise { + override buildCommandDocument(_connection: Connection, _session?: ClientSession): Document { const options = this.options ?? {}; const ordered = typeof options.ordered === 'boolean' ? options.ordered : true; const command: Document = { @@ -49,7 +48,7 @@ export class InsertOperation extends CommandOperation { command.comment = options.comment; } - return await super.executeCommand(server, session, command, timeoutContext); + return command; } } @@ -91,6 +90,20 @@ export class InsertOneOperation extends InsertOperation { insertedId: this.documents[0]._id }; } + + override handleOk(response: InstanceType): Document { + const res = super.handleOk(response); + if (res.code) throw new MongoServerError(res); + if (res.writeErrors) { + // This should be a WriteError but we can't change it now because of error hierarchy + throw new MongoServerError(res.writeErrors[0]); + } + + return { + acknowledged: this.writeConcern?.w !== 0, + insertedId: this.documents[0]._id + }; + } } /** @public */ diff --git a/src/operations/operation.ts b/src/operations/operation.ts index 190f2a522b..4171c689c1 100644 --- a/src/operations/operation.ts +++ b/src/operations/operation.ts @@ -1,7 +1,9 @@ +import { type Connection, type MongoError } from '..'; import { type BSONSerializeOptions, type Document, resolveBSONOptions } from '../bson'; +import { type MongoDBResponse } from '../cmap/wire_protocol/responses'; import { type Abortable } from '../mongo_types'; import { ReadPreference, type ReadPreferenceLike } from '../read_preference'; -import type { Server } from '../sdam/server'; +import type { Server, ServerCommandOptions } from '../sdam/server'; import type { ClientSession } from '../sessions'; import { type TimeoutContext } from '../timeout'; import type { MongoDBNamespace } from '../utils'; @@ -108,6 +110,10 @@ export abstract class AbstractOperation { return this._session; } + set session(session: ClientSession) { + this._session = session; + } + clearSession() { this._session = undefined; } @@ -125,6 +131,59 @@ export abstract class AbstractOperation { } } +/** @internal */ +export abstract class ModernizedOperation extends AbstractOperation { + abstract SERVER_COMMAND_RESPONSE_TYPE: typeof MongoDBResponse; + + /** this will never be used - but we must implement it to satisfy AbstractOperation's interface */ + override execute( + _server: Server, + _session: ClientSession | undefined, + _timeoutContext: TimeoutContext + ): Promise { + throw new Error('cannot execute!!'); + } + + /** + * Build a raw command document. + */ + abstract buildCommand(connection: Connection, session?: ClientSession): Document; + + /** + * Builds an instance of `ServerCommandOptions` to be used for operation execution. + */ + abstract buildOptions(timeoutContext: TimeoutContext): ServerCommandOptions; + + /** + * Given an instance of a MongoDBResponse, map the response to the correct result type. For + * example, a `CountOperation` might map the response as follows: + * + * ```typescript + * override handleOk(response: InstanceType): TResult { + * return response.toObject(this.bsonOptions).n ?? 0; + * } + * + * // or, with type safety: + * override handleOk(response: InstanceType): TResult { + * return response.getNumber('n') ?? 0; + * } + * ``` + */ + handleOk(response: InstanceType): TResult { + return response.toObject(this.bsonOptions) as TResult; + } + + /** + * Optional. + * + * If the operation performs error handling, such as wrapping, renaming the error, or squashing errors + * this method can be overridden. + */ + handleError(error: MongoError): TResult | never { + throw error; + } +} + export function defineAspects( operation: { aspects?: Set }, aspects: symbol | symbol[] | Set diff --git a/src/operations/search_indexes/drop.ts b/src/operations/search_indexes/drop.ts index 3b87bfad44..acbe3de9c6 100644 --- a/src/operations/search_indexes/drop.ts +++ b/src/operations/search_indexes/drop.ts @@ -1,13 +1,17 @@ +import { type Connection, type MongoError } from '../..'; import type { Document } from '../../bson'; +import { MongoDBResponse } from '../../cmap/wire_protocol/responses'; import type { Collection } from '../../collection'; import { MONGODB_ERROR_CODES, MongoServerError } from '../../error'; -import type { Server } from '../../sdam/server'; +import type { ServerCommandOptions } from '../../sdam/server'; import type { ClientSession } from '../../sessions'; import { type TimeoutContext } from '../../timeout'; -import { AbstractOperation } from '../operation'; +import { ModernizedOperation } from '../operation'; /** @internal */ -export class DropSearchIndexOperation extends AbstractOperation { +export class DropSearchIndexOperation extends ModernizedOperation { + override SERVER_COMMAND_RESPONSE_TYPE = MongoDBResponse; + private readonly collection: Collection; private readonly name: string; @@ -15,17 +19,14 @@ export class DropSearchIndexOperation extends AbstractOperation { super(); this.collection = collection; this.name = name; + this.ns = collection.fullNamespace; } override get commandName() { return 'dropSearchIndex' as const; } - override async execute( - server: Server, - session: ClientSession | undefined, - timeoutContext: TimeoutContext - ): Promise { + override buildCommand(_connection: Connection, _session?: ClientSession): Document { const namespace = this.collection.fullNamespace; const command: Document = { @@ -36,14 +37,22 @@ export class DropSearchIndexOperation extends AbstractOperation { command.name = this.name; } - try { - await server.command(namespace, command, { session, timeoutContext }); - } catch (error) { - const isNamespaceNotFoundError = - error instanceof MongoServerError && error.code === MONGODB_ERROR_CODES.NamespaceNotFound; - if (!isNamespaceNotFoundError) { - throw error; - } + return command; + } + + override handleOk(_response: MongoDBResponse): void { + // do nothing + } + + override buildOptions(timeoutContext: TimeoutContext): ServerCommandOptions { + return { session: this.session, timeoutContext }; + } + + override handleError(error: MongoError): void { + const isNamespaceNotFoundError = + error instanceof MongoServerError && error.code === MONGODB_ERROR_CODES.NamespaceNotFound; + if (!isNamespaceNotFoundError) { + throw error; } } } diff --git a/src/sdam/server.ts b/src/sdam/server.ts index 4d7052e327..bfaa9ac93d 100644 --- a/src/sdam/server.ts +++ b/src/sdam/server.ts @@ -38,6 +38,7 @@ import { import type { ServerApi } from '../mongo_client'; import { type Abortable, TypedEventEmitter } from '../mongo_types'; import type { GetMoreOptions } from '../operations/get_more'; +import { type ModernizedOperation } from '../operations/operation'; import type { ClientSession } from '../sessions'; import { type TimeoutContext } from '../timeout'; import { isTransactionCommand } from '../transactions'; @@ -277,6 +278,100 @@ export class Server extends TypedEventEmitter { } } + public async modernCommand( + operation: ModernizedOperation, + timeoutContext: TimeoutContext + ): Promise> { + if (this.s.state === STATE_CLOSING || this.s.state === STATE_CLOSED) { + throw new MongoServerClosedError(); + } + const session = operation.session; + + let conn = session?.pinnedConnection; + + this.incrementOperationCount(); + if (conn == null) { + try { + conn = await this.pool.checkOut({ timeoutContext }); + } catch (checkoutError) { + this.decrementOperationCount(); + if (!(checkoutError instanceof PoolClearedError)) this.handleError(checkoutError); + throw checkoutError; + } + } + + const cmd = operation.buildCommand(conn, session); + const options = operation.buildOptions(timeoutContext); + const ns = operation.ns; + + if (this.loadBalanced && isPinnableCommand(cmd, session) && !session?.pinnedConnection) { + session?.pin(conn); + } + + options.directConnection = this.topology.s.options.directConnection; + + // There are cases where we need to flag the read preference not to get sent in + // the command, such as pre-5.0 servers attempting to perform an aggregate write + // with a non-primary read preference. In this case the effective read preference + // (primary) is not the same as the provided and must be removed completely. + if (options.omitReadPreference) { + delete options.readPreference; + } + + if (this.description.iscryptd) { + options.omitMaxTimeMS = true; + } + + let reauthPromise: Promise | null = null; + + try { + try { + const res = await conn.command(ns, cmd, options, operation.SERVER_COMMAND_RESPONSE_TYPE); + throwIfWriteConcernError(res); + return res; + } catch (commandError) { + throw this.decorateCommandError(conn, cmd, options, commandError); + } + } catch (operationError) { + if ( + operationError instanceof MongoError && + operationError.code === MONGODB_ERROR_CODES.Reauthenticate + ) { + reauthPromise = this.pool.reauthenticate(conn); + reauthPromise.then(undefined, error => { + reauthPromise = null; + squashError(error); + }); + + await abortable(reauthPromise, options); + reauthPromise = null; // only reachable if reauth succeeds + + try { + const res = await conn.command(ns, cmd, options, operation.SERVER_COMMAND_RESPONSE_TYPE); + throwIfWriteConcernError(res); + return res; + } catch (commandError) { + throw this.decorateCommandError(conn, cmd, options, commandError); + } + } else { + throw operationError; + } + } finally { + this.decrementOperationCount(); + if (session?.pinnedConnection !== conn) { + if (reauthPromise != null) { + // The reauth promise only exists if it hasn't thrown. + const checkBackIn = () => { + this.pool.checkIn(conn); + }; + void reauthPromise.then(checkBackIn, checkBackIn); + } else { + this.pool.checkIn(conn); + } + } + } + } + public async command( ns: MongoDBNamespace, command: Document, diff --git a/test/integration/crud/abstract_operation.test.ts b/test/integration/crud/abstract_operation.test.ts index 052286a3ea..fe4d1d4758 100644 --- a/test/integration/crud/abstract_operation.test.ts +++ b/test/integration/crud/abstract_operation.test.ts @@ -290,6 +290,9 @@ describe('abstract operation', function () { if (!WrapperSubclasses.includes(subclassType.name.toString())) { it(`operation.commandName equals key in command document`, async function () { const subclassInstance = subclassCreator(); + if (subclassInstance instanceof mongodb.ModernizedOperation) { + return; + } const yieldDoc = subclassType.name === 'ProfilingLevelOperation' ? { ok: 1, was: 1 } : { ok: 1 }; const cmdCallerStub = sinon.stub(Server.prototype, 'command').resolves(yieldDoc); diff --git a/test/integration/mongodb-handshake/mongodb-handshake.prose.test.ts b/test/integration/mongodb-handshake/mongodb-handshake.prose.test.ts index 1cdd9e6d8f..b2d127fc9c 100644 --- a/test/integration/mongodb-handshake/mongodb-handshake.prose.test.ts +++ b/test/integration/mongodb-handshake/mongodb-handshake.prose.test.ts @@ -162,23 +162,25 @@ describe('Handshake Prose Tests', function () { let stubCalled = false; beforeEach(() => { // Mock the server response in a way that saslSupportedMechs array in the hello command response contains an arbitrary string. - sinon.stub(Connection.prototype, 'command').callsFake(async function (ns, cmd, options) { - // @ts-expect-error: sinon will place wrappedMethod there - const command = Connection.prototype.command.wrappedMethod.bind(this); - if (cmd.hello || cmd[LEGACY_HELLO_COMMAND]) { - return stub(); - } - return command(ns, cmd, options); - - async function stub() { - stubCalled = true; - const response = await command(ns, cmd, options); - return { - ...response, - saslSupportedMechs: [...(response.saslSupportedMechs ?? []), 'random string'] - }; - } - }); + sinon + .stub(Connection.prototype, 'command') + .callsFake(async function (ns, cmd, options, responseType) { + // @ts-expect-error: sinon will place wrappedMethod there + const command = Connection.prototype.command.wrappedMethod.bind(this); + if (cmd.hello || cmd[LEGACY_HELLO_COMMAND]) { + return stub(); + } + return command(ns, cmd, options, responseType); + + async function stub() { + stubCalled = true; + const response = await command(ns, cmd, options, responseType); + return { + ...response, + saslSupportedMechs: [...(response.saslSupportedMechs ?? []), 'random string'] + }; + } + }); }); afterEach(() => sinon.restore()); diff --git a/test/integration/retryable-writes/non-server-retryable_writes.test.ts b/test/integration/retryable-writes/non-server-retryable_writes.test.ts index cc04931e7a..453928e622 100644 --- a/test/integration/retryable-writes/non-server-retryable_writes.test.ts +++ b/test/integration/retryable-writes/non-server-retryable_writes.test.ts @@ -32,7 +32,7 @@ describe('Non Server Retryable Writes', function () { 'returns the original error with a PoolRequstedRetry label after encountering a WriteConcernError', { requires: { topology: 'replicaset' } }, async () => { - const serverCommandStub = sinon.stub(Server.prototype, 'command'); + const serverCommandStub = sinon.stub(Server.prototype, 'modernCommand'); serverCommandStub.onCall(0).rejects(new PoolClearedError('error')); serverCommandStub.onCall(1).returns( Promise.reject( diff --git a/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts b/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts index 039d81721e..827455804d 100644 --- a/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts +++ b/test/integration/retryable-writes/retryable_writes.spec.prose.test.ts @@ -275,14 +275,13 @@ describe('Retryable Writes Spec Prose', () => { 'when a retry attempt fails with an error labeled NoWritesPerformed, drivers MUST return the original error', { requires: { topology: 'replicaset', mongodb: '>=4.2.9' } }, async () => { - const serverCommandStub = sinon.stub(Server.prototype, 'command'); - serverCommandStub.onCall(0).returns( - Promise.reject( - new MongoWriteConcernError({ - errorLabels: ['RetryableWriteError'], - writeConcernError: { errmsg: 'ShutdownInProgress error', code: 91 } - }) - ) + const serverCommandStub = sinon.stub(Server.prototype, 'modernCommand'); + serverCommandStub.onCall(0).rejects( + new MongoWriteConcernError({ + errorLabels: ['RetryableWriteError'], + writeConcernError: { errmsg: 'ShutdownInProgress error', code: 91 }, + ok: 1 + }) ); serverCommandStub.onCall(1).returns( Promise.reject( diff --git a/test/integration/server-selection/operation_count.test.ts b/test/integration/server-selection/operation_count.test.ts index d02c18d02a..cf2ec4e5d7 100644 --- a/test/integration/server-selection/operation_count.test.ts +++ b/test/integration/server-selection/operation_count.test.ts @@ -120,7 +120,7 @@ describe('Server Operation Count Tests', function () { it('is zero after a successful command', testMetadata, async function () { const server = Array.from(client.topology.s.servers.values())[0]; expect(server.s.operationCount).to.equal(0); - const commandSpy = sinon.spy(server, 'command'); + const commandSpy = sinon.spy(server, 'modernCommand'); const incrementSpy = sinon.spy(server, 'incrementOperationCount'); const decrementSpy = sinon.spy(server, 'decrementOperationCount'); @@ -147,7 +147,7 @@ describe('Server Operation Count Tests', function () { const server = Array.from(client.topology.s.servers.values())[0]; expect(server.s.operationCount).to.equal(0); - const commandSpy = sinon.spy(server, 'command'); + const commandSpy = sinon.spy(server, 'modernCommand'); const error = await collection.insertOne({ count: 1 }).catch(e => e); @@ -171,7 +171,7 @@ describe('Server Operation Count Tests', function () { sinon .stub(ConnectionPool.prototype, 'checkOut') .rejects(new Error('unable to checkout connection')); - const commandSpy = sinon.spy(server, 'command'); + const commandSpy = sinon.spy(server, 'modernCommand'); const error = await collection.insertOne({ count: 1 }).catch(e => e);