From 7bf12a1c918fc7acc0f3f3e7225c42fa782b644a Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 5 Jul 2026 12:10:14 +1000 Subject: [PATCH] fix: Server crash on unhandled rejection when an email adapter fails to send --- spec/UserController.spec.js | 53 +++++++++++++++++++++++++++++++ src/Controllers/UserController.js | 30 +++++++++++------ 2 files changed, 73 insertions(+), 10 deletions(-) diff --git a/spec/UserController.spec.js b/spec/UserController.spec.js index 1993dde079..7875b89501 100644 --- a/spec/UserController.spec.js +++ b/spec/UserController.spec.js @@ -81,4 +81,57 @@ describe('UserController', () => { }); }); }); + + describe('email adapter send rejection (#8496)', () => { + it('logs and does not crash when the verification email adapter rejects', async () => { + const logged = resolvingPromise(); + const adapter = { + sendVerificationEmail: () => Promise.reject(new Error('boom')), + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => Promise.resolve(), + }; + await reconfigureServer({ + verifyUserEmails: true, + emailAdapter: adapter, + appName: 'test', + publicServerURL: 'http://localhost:8378/1', + }); + spyOn(require('../lib/logger').logger, 'error').and.callFake(msg => logged.resolve(msg)); + + const user = new Parse.User(); + user.setUsername('u8496a'); + user.setPassword('pass'); + user.setEmail('u8496a@example.com'); + await user.signUp(); + + const msg = await logged; + expect(msg).toContain('Failed to send verification email'); + }); + + it('logs and does not crash when the password reset email adapter rejects', async () => { + const logged = resolvingPromise(); + const adapter = { + sendVerificationEmail: () => Promise.resolve(), + sendPasswordResetEmail: () => Promise.reject(new Error('boom')), + sendMail: () => Promise.resolve(), + }; + await reconfigureServer({ + appName: 'test', + emailAdapter: adapter, + publicServerURL: 'http://localhost:8378/1', + }); + + const user = new Parse.User(); + user.setUsername('u8496b'); + user.setPassword('pass'); + user.setEmail('u8496b@example.com'); + await user.signUp(); + + spyOn(require('../lib/logger').logger, 'error').and.callFake(msg => logged.resolve(msg)); + await Parse.User.requestPasswordReset('u8496b@example.com'); + + const msg = await logged; + expect(msg).toContain('Failed to send password reset email'); + }); + }); }); diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index c8b74d2ab4..ade628e1f1 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -6,6 +6,7 @@ import rest from '../rest'; import Parse from 'parse/node'; import AccountLockout from '../AccountLockout'; import Config from '../Config'; +import logger from '../logger'; var RestQuery = require('../RestQuery'); var Auth = require('../Auth'); @@ -177,11 +178,16 @@ export class UserController extends AdaptableController { link: link, user: inflate('_User', fetchedUser), }; - if (this.adapter.sendVerificationEmail) { - this.adapter.sendVerificationEmail(options); - } else { - this.adapter.sendMail(this.defaultVerificationEmail(options)); - } + // Dispatch detached so the request does not block on email delivery, but attach a + // catch so a rejected send (e.g. bad SMTP config, throwing apiCallback) is logged + // instead of crashing the process with an unhandled rejection (#8496). + Promise.resolve() + .then(() => + this.adapter.sendVerificationEmail + ? this.adapter.sendVerificationEmail(options) + : this.adapter.sendMail(this.defaultVerificationEmail(options)) + ) + .catch(e => logger.error(`Failed to send verification email: ${e?.message || e}`, { error: e })); } /** @@ -289,11 +295,15 @@ export class UserController extends AdaptableController { user: inflate('_User', user), }; - if (this.adapter.sendPasswordResetEmail) { - this.adapter.sendPasswordResetEmail(options); - } else { - this.adapter.sendMail(this.defaultResetPasswordEmail(options)); - } + // Dispatch detached (see sendVerificationEmail) so a rejected send is logged rather + // than crashing the process with an unhandled rejection (#8496). + Promise.resolve() + .then(() => + this.adapter.sendPasswordResetEmail + ? this.adapter.sendPasswordResetEmail(options) + : this.adapter.sendMail(this.defaultResetPasswordEmail(options)) + ) + .catch(e => logger.error(`Failed to send password reset email: ${e?.message || e}`, { error: e })); return Promise.resolve(user); }