Skip to content

Remote Auth Sessions are not restoring after server reload #5781

@SajidHameed223

Description

@SajidHameed223

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions