From 19aa332d615728d667e97279ed9963c37d274f2d Mon Sep 17 00:00:00 2001 From: saubyk <39208279+saubyk@users.noreply.github.com> Date: Sun, 19 Jul 2026 16:16:36 -0700 Subject: [PATCH] Address review: protect the fire-and-forget channel close from the timeout LND's DELETE /v1/channels/{channelPoint} streams until the closing tx confirms, routinely longer than the wrapper's 10-minute bound. The close call in closeChannel is fire-and-forget (202 returned immediately, no .catch), so the timeout rejection would have become an unhandled promise rejection and crashed the process ~10 minutes after any close that had not yet confirmed. request-promise returned Bluebird promises whose unhandled rejections only warned, which is why this never crashed before. The close now uses a copy of the options with timeout: 0 (same treatment as the invoice/payment subscriptions) and a .catch that logs through handleError - errors were never surfaced to the HTTP response anyway, but logging beats Bluebird's silent warning. This was the only call site without a rejection handler. Verified on the fixture: opened a disposable 200k alice->bob channel via RTL, closed it (202, gone from open and listed in closed after mining), then requested a close for a bogus channel point - LND rejects the stream, the catch logs the error (no auth headers in it), and the process stays up. Read suite re-passes. --- backend/controllers/lnd/channels.js | 8 +++++++- server/controllers/lnd/channels.ts | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/backend/controllers/lnd/channels.js b/backend/controllers/lnd/channels.js index b6c19c1a..fdcdd8d9 100644 --- a/backend/controllers/lnd/channels.js +++ b/backend/controllers/lnd/channels.js @@ -175,7 +175,13 @@ export const closeChannel = (req, res, next) => { options.url = options.url + '&sat_per_vbyte=' + req.query.sat_per_vbyte; } logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Closing Channel Options URL', data: options.url }); - request.delete(options); + // Fire-and-forget: LND keeps the close stream open until the closing tx + // confirms, so exempt it from the request timeout; the 202 is already sent, + // so log a rejection instead of letting it crash the process. + request.delete({ ...options, timeout: 0 }).catch((errRes) => { + const err = common.handleError(errRes, 'Channels', 'Close Channel Error', req.session.selectedNode); + logger.log({ selectedNode: req.session.selectedNode, level: 'ERROR', fileName: 'Channels', msg: 'Close Channel Error', error: err }); + }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Channel Close Requested' }); res.status(202).json({ message: 'Close channel request has been submitted.' }); } diff --git a/server/controllers/lnd/channels.ts b/server/controllers/lnd/channels.ts index 6c50ab9a..24ad203b 100644 --- a/server/controllers/lnd/channels.ts +++ b/server/controllers/lnd/channels.ts @@ -169,7 +169,13 @@ export const closeChannel = (req, res, next) => { if (req.query.target_conf) { options.url = options.url + '&target_conf=' + req.query.target_conf; } if (req.query.sat_per_vbyte) { options.url = options.url + '&sat_per_vbyte=' + req.query.sat_per_vbyte; } logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Closing Channel Options URL', data: options.url }); - request.delete(options); + // Fire-and-forget: LND keeps the close stream open until the closing tx + // confirms, so exempt it from the request timeout; the 202 is already sent, + // so log a rejection instead of letting it crash the process. + request.delete({ ...options, timeout: 0 }).catch((errRes) => { + const err = common.handleError(errRes, 'Channels', 'Close Channel Error', req.session.selectedNode); + logger.log({ selectedNode: req.session.selectedNode, level: 'ERROR', fileName: 'Channels', msg: 'Close Channel Error', error: err }); + }); logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Channel Close Requested' }); res.status(202).json({ message: 'Close channel request has been submitted.' }); } catch (error: any) {