-
Notifications
You must be signed in to change notification settings - Fork 335
Description
bug description:
When creating a Cap instance with custom storage hooks (and/or noFSState: true),
the constructor still unconditionally registers process.once handlers for SIGINT, SIGTERM,
and SIGQUIT, as well as a process.on("beforeExit", ...) handler:
process.on("beforeExit", () => this.cleanup());
["SIGINT", "SIGTERM", "SIGQUIT"].forEach((signal) => {
process.once(signal, () => {
this.cleanup()
.then(() => process.exit(0))
.catch(() => process.exit(1));
});
});These handlers are meant to persist in-memory token state to disk before the process exits.
However, when using custom storage hooks, there is no filesystem state to flush — the cleanup
is a no-op from a persistence standpoint. The unconditional registration causes serious
conflicts with host application process lifecycle management.
In contrast, _loadTokens() is correctly guarded:
if (!this.config.noFSState && !this.config.storage?.tokens) {
this._loadTokens().catch(() => {});
}The same guard should apply to the exit/signal handlers.
affects:
- standalone
- widget (ui)
- widget (wasm solver)
- js server
- js solver
to reproduce:
Use Cap with custom storage hooks in a Node.js/Deno application that manages its own
process lifecycle (e.g. via on-shutdown,
which patches process.exit to run a graceful shutdown sequence):
import Cap from "@cap.js/server";
const cap = new Cap({
noFSState: true,
storage: {
challenges: { store, read, delete, deleteExpired },
tokens: { store, get, delete, deleteExpired },
},
});When an unhandledRejection occurs anywhere in the application before the host app
gets a chance to replace the signal handlers, on-shutdown's default unhandledRejection
handler fires shutdown(), which triggers application teardown. At that point
process.exit(0) is already patched to shutdown, so Cap's own SIGTERM handler also
calls shutdown() on signal delivery — resulting in double-shutdown or shutdown firing
before the application is fully initialized.
Observed in the fount project on Deno, CI log:
Initialize @cap.js/[email protected]
unhandledRejection
Error saving JSON file: undefined/config.json
Error: ENOENT: no such file or directory, open 'undefined/config.json'
at save_config (src/server/server.mjs:45:2)
at shutdown ([email protected]/main.mjs:13:9)
at Process.<anonymous> ([email protected]/main.mjs:29:9)
Failed CI run: https://github.com/steve02081504/fount/actions/runs/22748003361/job/65976006396
expected behavior:
Signal/exit handlers should only be registered when there is actual filesystem state to
clean up — i.e. when noFSState is false and no custom storage.tokens hooks
are provided. Suggested fix:
if (!this.config.noFSState && !this.config.storage?.tokens) {
this._loadTokens().catch(() => {});
process.on("beforeExit", () => this.cleanup());
["SIGINT", "SIGTERM", "SIGQUIT"].forEach((signal) => {
process.once(signal, () => {
this.cleanup()
.then(() => process.exit(0))
.catch(() => process.exit(1));
});
});
}versions and environment:
@cap.js/server: 4.0.5- Runtime: Deno 2.x (with Node.js compat layer), but also reproducible in Node.js when
process.exitis patched by a lifecycle management library - OS: Ubuntu (GitHub Actions
ubuntu-latest)
additional context:
The disableAutoCleanup option controls _lazyCleanup() inside methods, but has no
effect on these process-level signal registrations. There is currently no opt-out
mechanism for the signal handlers short of monkey-patching process.once before
constructing Cap.