New Issue Checklist
Issue Description
RedisCacheAdapter#get catches adapter errors, logs them and resolves. put, del and clear do not, so they reject when Redis is unavailable. Parse Server calls all three without awaiting them in six places, which turns a transient Redis failure into an unhandled promise rejection.
// src/Adapters/Cache/RedisCacheAdapter.js
async get(key) {
try {
…
} catch (err) {
logger.error('RedisCacheAdapter error on get', { error: err }); // handled
}
}
async del(key) {
await this.queue.enqueue(key);
return this.client.del(key); // rejects
}
The call sites that do not await, all of which sit on hot paths:
| Location |
Call |
Runs on |
src/Auth.js:140 |
cacheController.user.del(sessionToken) |
every expired-session auth |
src/Auth.js:203 |
cacheController.user.put(sessionToken, …) |
every session-token auth |
src/Auth.js:342 |
cacheController.role.put(user.id, …) |
every role-closure computation |
src/Auth.js:350 |
cacheController.role.del(user.id) |
clearRoleCache |
src/Auth.js:351 |
cacheController.user.del(sessionToken) |
clearRoleCache |
src/RestWrite.js:1566 |
cacheController.role.clear() |
every _Role write |
src/RestWrite.js:775 is not affected, its call is returned inside a promise chain.
Not awaiting is the correct design in each case, since a cache write must not delay or fail the request that triggered it. The defect is that the adapter rejects at all, given that its own get establishes the opposite contract.
Depending on the Node version and process configuration, an unhandled rejection either logs a warning or terminates the process, so a brief Redis outage during normal traffic can take down an otherwise healthy server. The failure also arrives with no context about which cache operation produced it, because there is no handler to name it.
This was raised in review on #10620 and fixed there for the one new call site that PR adds, with a .catch() at the call site. Fixing it in the adapter covers the six pre-existing sites at once, and any future caller.
Steps to reproduce
- Configure Parse Server with
RedisCacheAdapter.
- Make Redis unavailable, for example stop the server or point the adapter at a closed port.
- Authenticate with a session token, or save a
_Role.
A standalone reproduction, no Redis required, since a client that always rejects is what an outage looks like to the adapter:
const RedisCacheAdapter = require('parse-server/lib/Adapters/Cache/RedisCacheAdapter').default;
const cache = new RedisCacheAdapter(null, 100);
cache.client = {
set: () => Promise.reject(new Error('Redis is unavailable')),
del: () => Promise.reject(new Error('Redis is unavailable')),
sendCommand: () => Promise.reject(new Error('Redis is unavailable')),
};
process.on('unhandledRejection', reason => console.log('unhandled:', reason.message));
cache.put('k', 'v'); // not awaited, as in Auth.js
cache.del('k');
cache.clear();
Actual Outcome
unhandled: Redis is unavailable
unhandled: Redis is unavailable
unhandled: Redis is unavailable
Expected Outcome
put, del and clear behave like get: the error is logged and the promise resolves. No unhandled rejection, and the log names the operation that failed.
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):
not applicable, server-internal cache
- SDK version:
not applicable
Logs
Nothing is logged for the failing operation. The only output is the runtime's unhandled rejection warning.
New Issue Checklist
Issue Description
RedisCacheAdapter#getcatches adapter errors, logs them and resolves.put,delandcleardo not, so they reject when Redis is unavailable. Parse Server calls all three without awaiting them in six places, which turns a transient Redis failure into an unhandled promise rejection.The call sites that do not await, all of which sit on hot paths:
src/Auth.js:140cacheController.user.del(sessionToken)src/Auth.js:203cacheController.user.put(sessionToken, …)src/Auth.js:342cacheController.role.put(user.id, …)src/Auth.js:350cacheController.role.del(user.id)clearRoleCachesrc/Auth.js:351cacheController.user.del(sessionToken)clearRoleCachesrc/RestWrite.js:1566cacheController.role.clear()_Rolewritesrc/RestWrite.js:775is not affected, its call is returned inside a promise chain.Not awaiting is the correct design in each case, since a cache write must not delay or fail the request that triggered it. The defect is that the adapter rejects at all, given that its own
getestablishes the opposite contract.Depending on the Node version and process configuration, an unhandled rejection either logs a warning or terminates the process, so a brief Redis outage during normal traffic can take down an otherwise healthy server. The failure also arrives with no context about which cache operation produced it, because there is no handler to name it.
This was raised in review on #10620 and fixed there for the one new call site that PR adds, with a
.catch()at the call site. Fixing it in the adapter covers the six pre-existing sites at once, and any future caller.Steps to reproduce
RedisCacheAdapter._Role.A standalone reproduction, no Redis required, since a client that always rejects is what an outage looks like to the adapter:
Actual Outcome
Expected Outcome
put,delandclearbehave likeget: the error is logged and the promise resolves. No unhandled rejection, and the log names the operation that failed.Environment
Server
9.10.1-alpha.6macOS 15.5localDatabase
MongoDB8.0localClient
not applicable, server-internal cachenot applicableLogs
Nothing is logged for the failing operation. The only output is the runtime's unhandled rejection warning.