-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Description
Is there an existing issue for this?
- I have searched the existing issues.
Is this a problem caused by your code, or is it specifically because of the library?
- I have double-checked my code carefully.
Describe the bug.
I am using wwebjs RemoteAuth with mongoose and wwebjs-mongo and i saw my sessions are stored in the GridFS collections but I do not restore them on server reload.
Expected Behavior
Excepted Behaviour is that session restore after restarting server and i achieve this by customizing the zip and unzip process
import { RemoteAuth, Events } from "whatsapp-web.js";
import fs from "fs-extra";
import path from "path";
import archiver from "archiver";
export class CustomRemoteAuth extends RemoteAuth {
private isZipping = false;
private allowSync = false; // Internal flag to permit sync only when we want it (e.g. Sleep)
constructor(options: any) {
super(options);
}
/**
* DISABLE the internal periodic sync timer by overriding the method.
* This prevents the base RemoteAuth class from trying to zip the session while
* Puppeteer is running, which causes EBUSY locks on Windows.
*/
async periodicSync() {
// console.log(`[CustomRemoteAuth] Periodic sync blocked (Windows optimization)`);
// Do nothing. We use our manual "Sleep" sync instead.
}
/**
* Override periodic sync to catch errors (EBUSY on Windows)
* and prevent the "Unhandled Promise Rejection" crash.
*/
async storeRemoteSession(options?: any) {
// block ALL periodic syncs from the base class to prevent EBUSY on Windows
// ONLY allow when explicitly requested via { force: true }
if (!options || !options.force) {
return;
}
const pathExists = await (this as any).isValidPath((this as any).userDataDir);
if (!pathExists) return;
if (this.isZipping) return;
this.isZipping = true;
try {
await this.compressSession();
await (this as any).store.save({ session: (this as any).sessionName });
const zipPath = `${(this as any).sessionName}.zip`;
if (await fs.pathExists(zipPath)) {
await fs.promises.unlink(zipPath).catch(() => {});
}
await fs.promises.rm(`${(this as any).tempDir}`, {
recursive: true,
force: true,
maxRetries: (this as any).rmMaxRetries || 4,
}).catch(() => { });
if (options && options.emit) {
(this as any).client.emit(Events.REMOTE_SESSION_SAVED);
}
} catch (err: any) {
// Log the error but don't throw (prevents crash during periodic backup)
console.warn(`[CustomRemoteAuth] Periodic backup skipped: ${err.message}`);
// Cleanup in case of partial failure
const zipPath = `${(this as any).sessionName}.zip`;
if (await fs.pathExists(zipPath)) {
await fs.promises.unlink(zipPath).catch(() => { });
}
await fs.promises.rm(`${(this as any).tempDir}`, {
recursive: true,
force: true,
}).catch(() => { });
} finally {
this.isZipping = false;
}
}
/**
* A version of storeRemoteSession that THROWS on error.
* Use this when you MUST ensure a backup happens (e.g., during shutdown).
*/
async forceStoreRemoteSession() {
// Add a small delay for IndexedDB persistence (from community fix)
await new Promise(resolve => setTimeout(resolve, 3000));
// Add a timeout to the entire force-save operation to prevent blocking initialization forever
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error("Force sync timeout after 60s")), 60000)
);
const syncPromise = (async () => {
if (this.isZipping) {
// Wait for ongoing periodic zip to finish or timeout
let i = 0;
while (this.isZipping && i < 10) { await new Promise(r => setTimeout(r, 1000)); i++; }
}
this.isZipping = true;
this.allowSync = true;
try {
await this.compressSession();
await (this as any).store.save({ session: (this as any).sessionName });
const zipPath = `${(this as any).sessionName}.zip`;
if (await fs.pathExists(zipPath)) {
await fs.promises.unlink(zipPath).catch(() => {});
}
await fs.promises.rm(`${(this as any).tempDir}`, {
recursive: true,
force: true,
maxRetries: (this as any).rmMaxRetries || 4,
}).catch(() => { });
console.log(`[CustomRemoteAuth] Force sync successful (${(this as any).sessionName})`);
} finally {
this.isZipping = false;
this.allowSync = false;
}
})();
await Promise.race([syncPromise, timeoutPromise]);
}
async compressSession() {
const archive = archiver("zip");
const zipFile = path.resolve(`${(this as any).sessionName}.zip`);
const stream = fs.createWriteStream(zipFile);
try {
// FIX: Always clean tempDir BEFORE starting to avoid EEXIST/ENOENT race conditions
if (await fs.pathExists((this as any).tempDir)) {
await fs.remove((this as any).tempDir).catch(() => {});
}
if (await fs.pathExists((this as any).userDataDir)) {
// WINDOWS: Wait a bit for browser handles to release
await new Promise(r => setTimeout(r, 2000));
let copied = false;
for (let i = 0; i < 5; i++) {
try {
await fs.copy((this as any).userDataDir, (this as any).tempDir);
copied = true;
break;
} catch (e: any) {
console.warn(`[CustomRemoteAuth] Copy attempt ${i+1} failed: ${e.message}`);
await new Promise(r => setTimeout(r, 3000));
}
}
if (!copied) throw new Error("Failed to copy session data after 3 attempts");
} else {
throw new Error("userDataDir does not exist yet");
}
} catch (err: any) {
stream.end();
await fs.remove((this as any).tempDir).catch(() => { });
throw err;
}
await (this as any).deleteMetadata();
return new Promise<void>((resolve, reject) => {
archive
.directory((this as any).tempDir, false)
.on("error", (err: any) => reject(err))
.pipe(stream);
stream.on("close", async () => {
// WINDOWS CRITICAL: Wait 2 seconds for OS to settle file locks
await new Promise(r => setTimeout(r, 2000));
resolve();
});
archive.finalize();
});
}
}
Steps to Reproduce the Bug or Issue
import { RemoteAuth, Events } from "whatsapp-web.js";
import fs from "fs-extra";
import path from "path";
import archiver from "archiver";
export class CustomRemoteAuth extends RemoteAuth {
private isZipping = false;
private allowSync = false; // Internal flag to permit sync only when we want it (e.g. Sleep)
constructor(options: any) {
super(options);
}
/**
* DISABLE the internal periodic sync timer by overriding the method.
* This prevents the base RemoteAuth class from trying to zip the session while
* Puppeteer is running, which causes EBUSY locks on Windows.
*/
async periodicSync() {
// console.log(`[CustomRemoteAuth] Periodic sync blocked (Windows optimization)`);
// Do nothing. We use our manual "Sleep" sync instead.
}
/**
* Override periodic sync to catch errors (EBUSY on Windows)
* and prevent the "Unhandled Promise Rejection" crash.
*/
async storeRemoteSession(options?: any) {
// block ALL periodic syncs from the base class to prevent EBUSY on Windows
// ONLY allow when explicitly requested via { force: true }
if (!options || !options.force) {
return;
}
const pathExists = await (this as any).isValidPath((this as any).userDataDir);
if (!pathExists) return;
if (this.isZipping) return;
this.isZipping = true;
try {
await this.compressSession();
await (this as any).store.save({ session: (this as any).sessionName });
const zipPath = `${(this as any).sessionName}.zip`;
if (await fs.pathExists(zipPath)) {
await fs.promises.unlink(zipPath).catch(() => {});
}
await fs.promises.rm(`${(this as any).tempDir}`, {
recursive: true,
force: true,
maxRetries: (this as any).rmMaxRetries || 4,
}).catch(() => { });
if (options && options.emit) {
(this as any).client.emit(Events.REMOTE_SESSION_SAVED);
}
} catch (err: any) {
// Log the error but don't throw (prevents crash during periodic backup)
console.warn(`[CustomRemoteAuth] Periodic backup skipped: ${err.message}`);
// Cleanup in case of partial failure
const zipPath = `${(this as any).sessionName}.zip`;
if (await fs.pathExists(zipPath)) {
await fs.promises.unlink(zipPath).catch(() => { });
}
await fs.promises.rm(`${(this as any).tempDir}`, {
recursive: true,
force: true,
}).catch(() => { });
} finally {
this.isZipping = false;
}
}
/**
* A version of storeRemoteSession that THROWS on error.
* Use this when you MUST ensure a backup happens (e.g., during shutdown).
*/
async forceStoreRemoteSession() {
// Add a small delay for IndexedDB persistence (from community fix)
await new Promise(resolve => setTimeout(resolve, 3000));
// Add a timeout to the entire force-save operation to prevent blocking initialization forever
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error("Force sync timeout after 60s")), 60000)
);
const syncPromise = (async () => {
if (this.isZipping) {
// Wait for ongoing periodic zip to finish or timeout
let i = 0;
while (this.isZipping && i < 10) { await new Promise(r => setTimeout(r, 1000)); i++; }
}
this.isZipping = true;
this.allowSync = true;
try {
await this.compressSession();
await (this as any).store.save({ session: (this as any).sessionName });
const zipPath = `${(this as any).sessionName}.zip`;
if (await fs.pathExists(zipPath)) {
await fs.promises.unlink(zipPath).catch(() => {});
}
await fs.promises.rm(`${(this as any).tempDir}`, {
recursive: true,
force: true,
maxRetries: (this as any).rmMaxRetries || 4,
}).catch(() => { });
console.log(`[CustomRemoteAuth] Force sync successful (${(this as any).sessionName})`);
} finally {
this.isZipping = false;
this.allowSync = false;
}
})();
await Promise.race([syncPromise, timeoutPromise]);
}
async compressSession() {
const archive = archiver("zip");
const zipFile = path.resolve(`${(this as any).sessionName}.zip`);
const stream = fs.createWriteStream(zipFile);
try {
// FIX: Always clean tempDir BEFORE starting to avoid EEXIST/ENOENT race conditions
if (await fs.pathExists((this as any).tempDir)) {
await fs.remove((this as any).tempDir).catch(() => {});
}
if (await fs.pathExists((this as any).userDataDir)) {
// WINDOWS: Wait a bit for browser handles to release
await new Promise(r => setTimeout(r, 2000));
let copied = false;
for (let i = 0; i < 5; i++) {
try {
await fs.copy((this as any).userDataDir, (this as any).tempDir);
copied = true;
break;
} catch (e: any) {
console.warn(`[CustomRemoteAuth] Copy attempt ${i+1} failed: ${e.message}`);
await new Promise(r => setTimeout(r, 3000));
}
}
if (!copied) throw new Error("Failed to copy session data after 3 attempts");
} else {
throw new Error("userDataDir does not exist yet");
}
} catch (err: any) {
stream.end();
await fs.remove((this as any).tempDir).catch(() => { });
throw err;
}
await (this as any).deleteMetadata();
return new Promise<void>((resolve, reject) => {
archive
.directory((this as any).tempDir, false)
.on("error", (err: any) => reject(err))
.pipe(stream);
stream.on("close", async () => {
// WINDOWS CRITICAL: Wait 2 seconds for OS to settle file locks
await new Promise(r => setTimeout(r, 2000));
resolve();
});
archive.finalize();
});
}
}
WhatsApp Account Type
Standard
Browser Type
Google Chrome
Operation System Type
Windows
Phone OS Type
Andriod
WhatsApp-Web.js Version
1.34.4
WhatsApp Web Version
latest
Node.js Version
22
Authentication Strategy
RemoteAuth
Additional Context
No response