Running ncu -u (npm-check-updates) to upgrade all packages to their latest versions causes breakages in the following Wappler Server Connect core modules:
lib/server.js— breaks due to Express 5 + Node 22 async timing changeslib/setup/routes.js— breaks due to Express 5'spath-to-regexpv8 syntax changes and async timinglib/oauth/index.js— breaks due to Express 5 session save timing on redirectslib/setup/session.js— breaks due toconnect-redis9 requiringredis(node-redis) client instead ofioredisDockerfile— breaks due toargon2requiring native build tools not present in slim Docker images
The primary packages that cause these breakages when upgraded via ncu -u:
| Upgraded Package | Old → New | Modules Affected |
|---|---|---|
express | ^4.21.2 → ^5.2.1 | lib/server.js, lib/setup/routes.js, lib/oauth/index.js |
connect-redis | ^6.x → ^9.0.0 | lib/setup/session.js |
argon2 | not included → ^0.44.0 | Dockerfile (native build failure) |
This document details each breaking change and the fix applied.
Package Version Changes
| Package | Old Version (Wappler default) | New Version | Breaking? | Notes |
|---|---|---|---|---|
express | ^4.21.2 | ^5.2.1 | Yes | path-to-regexp v8, async timing changes |
connect-redis | ^6.x | ^9.0.0 | Yes | Named export { RedisStore }, requires redis (node-redis) client instead of ioredis |
redis (node-redis) | not included | ^5.11.0 | N/A | New dependency — required by connect-redis 9 for session store |
argon2 | not included (used bcryptjs) | ^0.44.0 | N/A | New dependency — faster & more secure password hashing, requires native build tools (python3, make, g++) in Docker |
| Node.js | 18 / 20 | 22 | Yes | New microtask/Promise scheduling exposes latent async bugs |
Breaking Changes & Required Fixes
1. Async Route Registration (lib/server.js)
Problem: The routes() function in lib/setup/routes.js is async but was called without await. In Express 4 with older Node.js, async operations happened to resolve synchronously, so routes were registered before the 404 handler by luck. With Node 22, Promise/microtask scheduling changed, causing the 404 catch-all handler to be registered before routes finished — resulting in 404 for all routes.
Fix:
// Before (Express 4)
upload(app);
secure(app);
routes(app);
auth(app);
module.exports = {
start: function({ port, httpsOptions } = {}) {
const server = httpsOptions ? ...
// After (Express 5)
upload(app);
secure(app);
let routesReady = routes(app);
auth(app);
module.exports = {
start: async function({ port, httpsOptions } = {}) {
await routesReady;
const server = httpsOptions ? ...
Root Cause: Latent bug — routes() returns a Promise that was never awaited. Node 22's microtask timing exposed it.
2. Async API Route Creation (lib/setup/routes.js)
Problem: Inside routes.js, createApiRoutes('app/api') uses map() from lib/core/async.js which wraps Promise.all — making it async. Without await, API routes (e.g., /api/admin/login) were not registered by the time the routes() function's Promise resolved.
Fix:
// Before (Express 4)
if (config.createApiRoutes) {
fs.ensureDirSync('app/api');
createApiRoutes('app/api');
}
// After (Express 5)
if (config.createApiRoutes) {
fs.ensureDirSync('app/api');
await createApiRoutes('app/api');
}
Root Cause: Same as #1 — unresolved Promise, exposed by Node 22 timing changes.
3. Route Path Syntax (lib/setup/routes.js)
Problem: Express 5 upgraded path-to-regexp from v0.1.x to v8.x, which no longer supports regex-style optional groups in route paths. Routes like /api/add_applicant(.json)? threw PathError: Unexpected ( at index 18.
Fix:
// Before (Express 4 — path-to-regexp v0.1.x)
let routePath = path.replace(/^app/i, '').replace(/.json$/, '(.json)?');
// After (Express 5 — path-to-regexp v8.x)
let routePath = path.replace(/^app/i, '').replace(/.json$/, '{.json}');</code></pre>
path-to-regexp v8 syntax changes:
| Express 4 (v0.1.x) | Express 5 (v8.x) | Description |
|---|---|---|
(.json)? | {.json} | Optional static text |
/:param? | {/:param} | Optional parameter |
(.*) | {*path} | Wildcard/catch-all |
Reference: https://github.com/pillarjs/path-to-regexp/releases
4. OAuth Session Persistence (lib/oauth/index.js)
Problem: The OAuth flow sets a state value in the session before redirecting to the OAuth provider. In Express 4, express-session reliably saved the session via its res.end() hook before the redirect response was sent. In Express 5 with Node 22, the redirect response could be sent before the session was written to the store, causing the state check to fail on callback — resulting in an infinite redirect loop (ERR_TOO_MANY_REDIRECTS).
Fix:
// Before (Express 4)
this.app.setSession(`${this.name}_state`, params.state);
this.app.res.redirect(build_url(this.opts.auth_endpoint, params));
// After (Express 5)
this.app.setSession(`${this.name}_state`, params.state);
// Explicitly save session before redirect to ensure state persists
await new Promise((resolve, reject) => {
this.app.req.session.save((err) => {
if (err) return reject(err);
resolve();
});
});
this.app.res.redirect(build_url(this.opts.auth_endpoint, params));
Root Cause: res.end() hook timing changed in Express 5 / Node 22 — session auto-save no longer guaranteed before redirect completes.
5. connect-redis 9: Requires redis (node-redis) Client (lib/setup/session.js)
Problem: connect-redis v9 uses the redis (node-redis v4+) client API: client.set(key, value, { EX: ttl }). Wappler's default setup passes an ioredis client (global.redisClient) which doesn't support the object options syntax — it stringifies the options to [object Object], causing Redis to reject the command.
Docker logs showed:
ReplyError: ERR syntax error
command: {
name: 'set',
args: [
'sess:kEVJPHiC...',
'{"cookie":...}',
'[object Object]' <-- should be 'EX', 900
]
}
Fix:
// Before (connect-redis 6 — uses ioredis via .default export)if (options.store.$type == 'redis') {
const RedisStore = require('connect-redis').default;
options.store = new RedisStore(Object.assign({
client: global.redisClient // ioredis client — incompatible with connect-redis v9
}, options.store));
}
// After (connect-redis 9 — uses node-redis via named export)
if (options.store.$type == 'redis') {
const { RedisStore } = require('connect-redis');
const { createClient } = require('redis');
const redisUrl = config.redis === true ? 'redis://redis' : config.redis;
const sessionRedisClient = createClient({ url: redisUrl });
sessionRedisClient.connect().catch(err => console.error('Session Redis connect error:', err));
const storeOpts = { ...options.store };
delete storeOpts.$type;
options.store = new RedisStore(Object.assign({
client: sessionRedisClient // node-redis client — compatible with connect-redis v9
}, storeOpts));
}
Key differences:
- Import:
require('connect-redis').default(v6) →{ RedisStore } = require('connect-redis')(v9) - Client:
ioredis(global.redisClient) →redis(node-redis v4+) with separatecreateClient() - Note:
global.redisClient(ioredis) is still used for other Redis operations (Socket.IO adapter, rate limiting, etc.). Only the session store needs theredispackage client.
6. Dockerfile: argon2 Native Build Dependencies
Problem: The argon2 npm package requires native compilation (uses node-gyp with C++ bindings). The Slim Deployment Wappler Dockerfile are based on node:20-bullseye-slim and do not include the necessary build tools, causing npm install to fail with build errors.
Fix — add to Dockerfile before npm install:
RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
Dockerfile example:
FROM node:22-bullseye-slim
ARG NODE_ENV=production
ENV NODE_ENV $NODE_ENV
ARG PORT=3000
ENV PORT $PORT
EXPOSE $PORT
RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
ENV PATH /opt/node_app/node_modules/.bin:$PATH
ENV NODE_ENV development
WORKDIR /opt/node_app
COPY index.js .
COPY package.json .
RUN npm install --no-optional --no-package-lock
CMD [ "nodemon", "./index.js" ]
Note: This is required for any project using argon2 for password hashing. The build tools (python3, make, g++) are needed by node-gyp to compile the native addon.
Files Changes that are required
| File | Change | Why |
|---|---|---|
lib/server.js | await routesReady before starting server | Node 22 microtask timing exposes unresolved Promise |
lib/setup/routes.js | await createApiRoutes() + {.json} path syntax | Async route registration + path-to-regexp v8 |
lib/oauth/index.js | Explicit session.save() before OAuth redirect | Session auto-save timing changed in Express 5 |
lib/setup/session.js | Use redis (node-redis) client + named import for RedisStore | connect-redis 9 requires node-redis client |
Dockerfile | Add python3 make g++ build tools | Required for argon2 native compilation |
New Dependencies Required
| Package | Purpose |
|---|---|
redis (node-redis) ^5.11.0 | Session store client for connect-redis 9 |
Recommendation
These changes are backward-compatible patterns (explicit await, explicit session.save()) that would also improve reliability on Express 4. The only breaking change is the connect-redis import/client switch and the path-to-regexp syntax. Would recommend the team to consider below changes for support of the new modules:
- Updating the core library files with the fixes above
- Adding
redis(node-redis) as a dependency alongsideioredis - Adding build tools to the Dockerfile template when
argon2is detected as a dependency - Updating path-to-regexp syntax in route generation for Express 5 compatibility