diff --git a/lib/base/transaction.js b/lib/base/transaction.js index 69654a23..1e5ccf1d 100644 --- a/lib/base/transaction.js +++ b/lib/base/transaction.js @@ -152,6 +152,7 @@ class Transaction extends EventEmitter { } this._aborted = false + this._abortReason = null this._rollbackRequested = false if (isolationLevel) { if (Object.keys(ISOLATION_LEVEL).some(key => { @@ -199,6 +200,22 @@ class Transaction extends EventEmitter { }) } + /** + * Creates a TransactionError for an aborted transaction, preserving the + * original abort reason (if any) as `originalError`. + * + * @private + * @return {TransactionError} + */ + + _createAbortError () { + const err = new TransactionError('Transaction has been aborted.', 'EABORT') + if (this._abortReason) { + Object.defineProperty(err, 'originalError', { enumerable: true, value: this._abortReason }) + } + return err + } + /** * @private * @param {basicCallback} [callback] @@ -207,7 +224,7 @@ class Transaction extends EventEmitter { _commit (callback) { if (this._aborted) { - return setImmediate(callback, new TransactionError('Transaction has been aborted.', 'EABORT')) + return setImmediate(callback, this._createAbortError()) } if (!this._acquiredConnection) { @@ -279,7 +296,7 @@ class Transaction extends EventEmitter { _rollback (callback) { if (this._aborted) { - return setImmediate(callback, new TransactionError('Transaction has been aborted.', 'EABORT')) + return setImmediate(callback, this._createAbortError()) } if (!this._acquiredConnection) { diff --git a/lib/tedious/request.js b/lib/tedious/request.js index 73d8afec..d9832a29 100644 --- a/lib/tedious/request.js +++ b/lib/tedious/request.js @@ -300,6 +300,9 @@ class Request extends BaseRequest { } catch (e) { // noop } + if (err && this.parent._aborted) { + this.parent._abortReason = err + } callback(err, ...args) } if (err) return callbackWithRelease(err) @@ -501,6 +504,9 @@ class Request extends BaseRequest { } this.parent.release(connection) + if (error && this.parent._aborted) { + this.parent._abortReason = error + } hasReturned = true if (error) { @@ -865,6 +871,9 @@ class Request extends BaseRequest { } this.parent.release(connection) + if (error && this.parent._aborted) { + this.parent._abortReason = error + } hasReturned = true if (error) { diff --git a/lib/tedious/transaction.js b/lib/tedious/transaction.js index 135d54f9..29718dfa 100644 --- a/lib/tedious/transaction.js +++ b/lib/tedious/transaction.js @@ -23,6 +23,7 @@ class Transaction extends BaseTransaction { this._acquiredConnection = null this._acquiredConfig = null this._aborted = true + this._abortReason = this._abortReason || new Error('Transaction was rolled back by the server') publish(CHANNELS.TRANSACTION_ROLLBACK, () => ({ transactionId: IDS.get(this), diff --git a/test/common/unit.js b/test/common/unit.js index 4757c253..2f50dfde 100644 --- a/test/common/unit.js +++ b/test/common/unit.js @@ -1402,4 +1402,63 @@ describe('connection string auth - tedious', () => { }) }) }) + + describe('Transaction EABORT originalError', () => { + const BaseTransaction = require('../../lib/base/transaction') + + it('commit on aborted transaction returns EABORT with originalError', (done) => { + const tx = new BaseTransaction(null) + tx._aborted = true + const reason = new Error('deadlock victim') + reason.code = 'EREQUEST' + tx._abortReason = reason + + tx.commit((err) => { + assert.ok(err) + assert.strictEqual(err.code, 'EABORT') + assert.strictEqual(err.message, 'Transaction has been aborted.') + assert.strictEqual(err.originalError, reason) + assert.strictEqual(err.originalError.message, 'deadlock victim') + done() + }) + }) + + it('rollback on aborted transaction returns EABORT with originalError', (done) => { + const tx = new BaseTransaction(null) + tx._aborted = true + const reason = new Error('constraint violation') + tx._abortReason = reason + + tx.rollback((err) => { + assert.ok(err) + assert.strictEqual(err.code, 'EABORT') + assert.strictEqual(err.message, 'Transaction has been aborted.') + assert.strictEqual(err.originalError, reason) + done() + }) + }) + + it('commit on aborted transaction without _abortReason omits originalError', (done) => { + const tx = new BaseTransaction(null) + tx._aborted = true + + tx.commit((err) => { + assert.ok(err) + assert.strictEqual(err.code, 'EABORT') + assert.strictEqual(err.originalError, undefined) + done() + }) + }) + + it('_abortReason is reset on begin', (done) => { + const tx = new BaseTransaction(null) + tx._abortReason = new Error('old reason') + tx._begin(undefined, (err) => { + assert.ifError(err) + assert.strictEqual(tx._abortReason, null) + assert.strictEqual(tx._aborted, false) + done() + }) + }) + }) })