parse-community / parse-community/parse-server

Verification and password reset emails are dispatched without a rejection handler, producing unhandled rejections

Open
#10,636 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
JavaScript
Stars
21.4k
Forks
4.8k
Avg merge
7h 45m
Merged PRs (30d)
11

Description

New Issue Checklist
Issue Description

Verification and password reset emails are dispatched without awaiting the mail adapter and without a rejection handler anywhere in the chain. A mail adapter that rejects, which is what an outage at the email provider looks like, produces an unhandled promise rejection.

The adapter call itself is never awaited, in either flow and in both the adapter-specific and sendMail fallback branches:

// src/Controllers/UserController.js
if (this.adapter.sendVerificationEmail) {
  this.adapter.sendVerificationEmail(options);              // 181
} else {
  this.adapter.sendMail(this.defaultVerificationEmail(options));   // 183
}

if (this.adapter.sendPasswordResetEmail) {
  this.adapter.sendPasswordResetEmail(options);             // 293
} else {
  this.adapter.sendMail(this.defaultResetPasswordEmail(options));  // 295
}

Several callers of the surrounding controller methods are also not awaited:

Location Call Runs on
src/RestWrite.js:1214 userController.sendVerificationEmail(…) user signup
src/Routers/UsersRouter.js:685 userController.sendVerificationEmail(user, req) POST /verificationEmailRequest
src/Controllers/UserController.js:227 this.sendVerificationEmail(aUser, req) resend from the verification page

UsersRouter.handleResetRequest at line 630 does await userController.sendPasswordResetEmail(email), but that await covers only token generation, since the adapter call inside resolves before delivery is attempted.

I want to separate two things here, because I think only one of them is unambiguously a defect.

The unhandled rejection is a defect. No .catch() exists at any level, so a rejecting mail adapter surfaces as an unhandled rejection with no indication of which email failed or for which user. Depending on the Node version and process configuration that either logs a bare warning or terminates the process, so an outage at the email provider can take down an otherwise healthy server. This is the same shape as #10634, where RedisCacheAdapter write methods reject into unawaited call sites.

Whether the client should learn about the failure is a design question, and I am not assuming the answer. The signup path is explicitly commented // Fire and forget! (src/RestWrite.js:1213), so the non-blocking dispatch is deliberate, and there are good reasons for it. Blocking signup on an email provider is a poor tradeoff, and for the password reset flow specifically, awaiting delivery leaks timing that helps an attacker distinguish registered from unregistered addresses, which is the enumeration exposure that resetPasswordSuccessOnInvalidEmail exists to close. So "return 200 regardless" may well be the intended contract.

What that leaves is a question for maintainers: should a failed send at minimum be logged with the adapter error and the target flow, so operators can detect a broken email pipeline, while delivery stays non-blocking? Today a completely dead mail adapter is silent in the logs, and the only signal is a stray unhandled rejection warning with no context.

Steps to reproduce
  1. Configure Parse Server with a mail adapter whose sendVerificationEmail or sendPasswordResetEmail returns a rejected promise.
  2. Sign up a user with verifyUserEmails: true, or call POST /requestPasswordReset.
const server = await reconfigureServer({
  verifyUserEmails: true,
  publicServerURL: 'http://localhost:8378/1',
  emailAdapter: {
    sendVerificationEmail: () => Promise.reject(new Error('Email provider is down')),
    sendPasswordResetEmail: () => Promise.reject(new Error('Email provider is down')),
    sendMail: () => Promise.reject(new Error('Email provider is down')),
  },
});

process.on('unhandledRejection', reason => console.log('unhandled:', reason.message));

const user = new Parse.User();
user.setUsername('zebra');
user.setPassword('password');
user.setEmail('zebra@example.com');
await user.signUp();
Actual Outcome

The request succeeds. unhandled: Email provider is down is printed. Nothing is written to the Parse Server log identifying the failed email, the flow it belonged to, or the user it was addressed to.

Expected Outcome

At minimum, the rejection is caught and logged with the adapter error and enough context to identify the flow, and no unhandled rejection is produced. Whether the response to the client should change is the maintainers' call, and I would not change it without direction, given the enumeration tradeoff on the password reset path.

Happy to open a PR once there is a decision on scope.

Environment

Server

  • Parse Server version: 9.10.1-alpha.6
  • Operating system: macOS 15.5
  • Local or remote host: local

Database

  • System (MongoDB or Postgres): MongoDB
  • Database version: 8.0
  • Local or remote host: local

Client

  • SDK (iOS, Android, JavaScript, PHP, Unity, etc): JavaScript
  • SDK version: as vendored by parse-server
Logs

No Parse Server log entry is produced for the failed send. The only output is the runtime's unhandled rejection warning.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the mail dispatch branches in src/Controllers/UserController.js and trace the listed callers in src/RestWrite.js and src/Routers/UsersRouter.js. Reproduce the rejected-adapter case from the issue and inspect existing logging and email tests before confirming the intended non-blocking behavior with maintainers. Done means the agreed scope is covered without an unhandled rejection and with the expected diagnostic context.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, node.js
Domain
api, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.