Address review feedback: fix options race, error handling, release notes

This commit is contained in:
Osuji 2026-07-29 08:12:01 +01:00
parent 3491a92300
commit dcad5673e9
5 changed files with 83 additions and 52 deletions

View file

@ -4,10 +4,10 @@ import { Common } from '../../utils/common.js';
let options = null;
const logger = Logger;
const common = Common;
export const getAliasForChannel = (selNode, channel) => {
export const getAliasForChannel = (selNode, channel, requestOptions) => {
const pubkey = (channel.remote_pubkey) ? channel.remote_pubkey : (channel.remote_node_pub) ? channel.remote_node_pub : '';
options.url = selNode.settings.lnServerUrl + '/v1/graph/node/' + pubkey;
return request(options).then((aliasBody) => {
requestOptions.url = selNode.settings.lnServerUrl + '/v1/graph/node/' + pubkey;
return request(requestOptions).then((aliasBody) => {
logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Channels', msg: 'Alias Received', data: aliasBody.node.alias });
channel.remote_alias = aliasBody.node.alias && aliasBody.node.alias !== '' ? aliasBody.node.alias : aliasBody.node.pub_key.slice(0, 20);
return channel;
@ -36,16 +36,17 @@ export const getAllChannels = (req, res, next) => {
total = local + remote;
channel.balancedness = (total === 0) ? 1 : (1 - Math.abs((local - remote) / total)).toFixed(3);
});
const getChannelAliasesTasks = body.channels.map((channel) => () => getAliasForChannel(req.session.selectedNode, channel));
const requestOptions = { ...options };
const getChannelAliasesTasks = body.channels.map((channel) => () => getAliasForChannel(req.session.selectedNode, channel, requestOptions));
common.runWithConcurrencyLimit(getChannelAliasesTasks, 20, () => {
try {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Sorted Channels List Received', data: body });
return res.status(200).json(body);
}
catch (e) {
const err = common.handleError(e, 'Channels', 'Get All Channel Aliases Error', req.session.selectedNode);
logger.log({ selectedNode: req.session.selectedNode, level: 'ERROR', fileName: 'Channels', msg: 'Get All Channel Aliases Error', error: e.message });
if (!res.headersSent) {
res.status(err.statusCode).json({ message: err.message, error: err.error });
res.status(500).json({ message: 'Get All Channel Aliases Error', error: e.message });
}
}
});
@ -72,18 +73,19 @@ export const getPendingChannels = (req, res, next) => {
if (!body.total_limbo_balance) {
body.total_limbo_balance = 0;
}
const requestOptions = { ...options };
const getPendingAliasesTasks = [];
if (body.pending_open_channels && body.pending_open_channels.length > 0) {
body.pending_open_channels?.map((channel) => getPendingAliasesTasks.push(() => getAliasForChannel(req.session.selectedNode, channel.channel)));
body.pending_open_channels?.map((channel) => getPendingAliasesTasks.push(() => getAliasForChannel(req.session.selectedNode, channel.channel, requestOptions)));
}
if (body.pending_force_closing_channels && body.pending_force_closing_channels.length > 0) {
body.pending_force_closing_channels?.map((channel) => getPendingAliasesTasks.push(() => getAliasForChannel(req.session.selectedNode, channel.channel)));
body.pending_force_closing_channels?.map((channel) => getPendingAliasesTasks.push(() => getAliasForChannel(req.session.selectedNode, channel.channel, requestOptions)));
}
if (body.pending_closing_channels && body.pending_closing_channels.length > 0) {
body.pending_closing_channels?.map((channel) => getPendingAliasesTasks.push(() => getAliasForChannel(req.session.selectedNode, channel.channel)));
body.pending_closing_channels?.map((channel) => getPendingAliasesTasks.push(() => getAliasForChannel(req.session.selectedNode, channel.channel, requestOptions)));
}
if (body.waiting_close_channels && body.waiting_close_channels.length > 0) {
body.waiting_close_channels?.map((channel) => getPendingAliasesTasks.push(() => getAliasForChannel(req.session.selectedNode, channel.channel)));
body.waiting_close_channels?.map((channel) => getPendingAliasesTasks.push(() => getAliasForChannel(req.session.selectedNode, channel.channel, requestOptions)));
}
common.runWithConcurrencyLimit(getPendingAliasesTasks, 20, () => {
try {
@ -91,9 +93,9 @@ export const getPendingChannels = (req, res, next) => {
return res.status(200).json(body);
}
catch (e) {
const err = common.handleError(e, 'Channels', 'Get Pending Channel Aliases Error', req.session.selectedNode);
logger.log({ selectedNode: req.session.selectedNode, level: 'ERROR', fileName: 'Channels', msg: 'Get Pending Channel Aliases Error', error: e.message });
if (!res.headersSent) {
res.status(err.statusCode).json({ message: err.message, error: err.error });
res.status(500).json({ message: 'Get Pending Channel Aliases Error', error: e.message });
}
}
});
@ -115,16 +117,17 @@ export const getClosedChannels = (req, res, next) => {
body.channels.forEach((channel) => {
channel.close_type = (!channel.close_type) ? 'COOPERATIVE_CLOSE' : channel.close_type;
});
const getClosedAliasesTasks = body.channels.map((channel) => () => getAliasForChannel(req.session.selectedNode, channel));
const requestOptions = { ...options };
const getClosedAliasesTasks = body.channels.map((channel) => () => getAliasForChannel(req.session.selectedNode, channel, requestOptions));
common.runWithConcurrencyLimit(getClosedAliasesTasks, 20, () => {
try {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Closed Channels List Received', data: body });
return res.status(200).json(body);
}
catch (e) {
const err = common.handleError(e, 'Channels', 'Get Closed Channel Aliases Error', req.session.selectedNode);
logger.log({ selectedNode: req.session.selectedNode, level: 'ERROR', fileName: 'Channels', msg: 'Get Closed Channel Aliases Error', error: e.message });
if (!res.headersSent) {
res.status(err.statusCode).json({ message: err.message, error: err.error });
res.status(500).json({ message: 'Get Closed Channel Aliases Error', error: e.message });
}
}
});

View file

@ -4,9 +4,9 @@ import { Common } from '../../utils/common.js';
let options = null;
const logger = Logger;
const common = Common;
export const getAliasFromPubkey = (selNode, pubkey) => {
options.url = selNode.settings.lnServerUrl + '/v1/graph/node/' + pubkey;
return request(options).then((res) => {
export const getAliasFromPubkey = (selNode, pubkey, requestOptions) => {
requestOptions.url = selNode.settings.lnServerUrl + '/v1/graph/node/' + pubkey;
return request(requestOptions).then((res) => {
logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Graph', msg: 'Alias Received', data: res.node.alias });
return res.node.alias;
}).
@ -83,21 +83,22 @@ export const getQueryRoutes = (req, res, next) => {
request(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Graph', msg: 'Query Routes Received', data: body });
if (body.routes && body.routes.length && body.routes.length > 0 && body.routes[0].hops && body.routes[0].hops.length && body.routes[0].hops.length > 0) {
const getRouteAliasesTasks = body.routes[0].hops.map((hop) => () => getAliasFromPubkey(req.session.selectedNode, hop.pub_key));
const requestOptions = { ...options };
const getRouteAliasesTasks = body.routes[0].hops.map((hop) => () => getAliasFromPubkey(req.session.selectedNode, hop.pub_key, requestOptions));
common.runWithConcurrencyLimit(getRouteAliasesTasks, 20, (values) => {
try {
body.routes[0].hops?.map((hop, i) => {
hop.hop_sequence = i + 1;
hop.pubkey_alias = values[i]?.error ? 'Unknown' : values[i];
hop.pubkey_alias = typeof values[i] === 'string' ? values[i] : 'Unknown';
return hop;
});
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Graph', msg: 'Graph Routes with Alias Received', data: body });
res.status(200).json(body);
}
catch (e) {
const err = common.handleError(e, 'Graph', 'Get Query Routes Error', req.session.selectedNode);
logger.log({ selectedNode: req.session.selectedNode, level: 'ERROR', fileName: 'Graph', msg: 'Get Query Routes Error', error: e.message });
if (!res.headersSent) {
res.status(err.statusCode).json({ message: err.message, error: err.error });
res.status(500).json({ message: 'Get Query Routes Error', error: e.message });
}
}
});
@ -149,17 +150,18 @@ export const getAliasesForPubkeys = (req, res, next) => {
}
if (req.query.pubkeys) {
const pubkeyArr = req.query.pubkeys.split(',');
const getAliasesTasks = pubkeyArr.map((pubkey) => () => getAliasFromPubkey(req.session.selectedNode, pubkey));
const requestOptions = { ...options };
const getAliasesTasks = pubkeyArr.map((pubkey) => () => getAliasFromPubkey(req.session.selectedNode, pubkey, requestOptions));
common.runWithConcurrencyLimit(getAliasesTasks, 20, (values) => {
try {
const safeValues = values.map((v) => (v?.error ? 'Unknown' : v));
const safeValues = values.map((v) => (typeof v === 'string' ? v : 'Unknown'));
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Graph', msg: 'Node Alias', data: safeValues });
res.status(200).json(safeValues);
}
catch (e) {
const err = common.handleError(e, 'Graph', 'Get Aliases for Pubkeys Error', req.session.selectedNode);
logger.log({ selectedNode: req.session.selectedNode, level: 'ERROR', fileName: 'Graph', msg: 'Get Aliases for Pubkeys Error', error: e.message });
if (!res.headersSent) {
res.status(err.statusCode).json({ message: err.message, error: err.error });
res.status(500).json({ message: 'Get Aliases for Pubkeys Error', error: e.message });
}
}
});

View file

@ -0,0 +1,21 @@
# Release Notes — 0.15.10
This document collects the changes that go into the 0.15.10 release. Each PR merged for
this release should add its entry under the appropriate section below.
## Code Health
- **Fix per-request options race condition in alias-resolution fan-outs**
([#1651](https://github.com/Ride-The-Lightning/RTL/pull/1651), fixes
[#1630](https://github.com/Ride-The-Lightning/RTL/issues/1630)).
The module-level `options` variable in `server/controllers/lnd/channels.ts` and
`server/controllers/lnd/graph.ts` was reassigned per-request via
`options = common.getOptions(req)`, but `getAliasForChannel` and `getAliasFromPubkey`
read it by closure rather than receiving it as a parameter. Once alias-resolution tasks
were deferred across event-loop turns by `runWithConcurrencyLimit`, a concurrent request
to a different handler or node could overwrite `options` mid-fan-out, causing a task to
send with the wrong node's credentials or URL. The two functions now accept an explicit
`requestOptions` parameter, and each handler captures a per-request shallow copy before
building the task thunks. The catch blocks inside the concurrency-limit callbacks now
log raw exceptions directly instead of routing them through `handleError` (which expects
an HTTP-error-shaped value), matching the pattern used by `closeChannel`.

View file

@ -6,10 +6,10 @@ let options = null;
const logger: LoggerService = Logger;
const common: CommonService = Common;
export const getAliasForChannel = (selNode: SelectedNode, channel) => {
export const getAliasForChannel = (selNode: SelectedNode, channel, requestOptions) => {
const pubkey = (channel.remote_pubkey) ? channel.remote_pubkey : (channel.remote_node_pub) ? channel.remote_node_pub : '';
options.url = selNode.settings.lnServerUrl + '/v1/graph/node/' + pubkey;
return request(options).then((aliasBody) => {
requestOptions.url = selNode.settings.lnServerUrl + '/v1/graph/node/' + pubkey;
return request(requestOptions).then((aliasBody) => {
logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Channels', msg: 'Alias Received', data: aliasBody.node.alias });
channel.remote_alias = aliasBody.node.alias && aliasBody.node.alias !== '' ? aliasBody.node.alias : aliasBody.node.pub_key.slice(0, 20);
return channel;
@ -37,14 +37,15 @@ export const getAllChannels = (req, res, next) => {
total = local + remote;
channel.balancedness = (total === 0) ? 1 : (1 - Math.abs((local - remote) / total)).toFixed(3);
});
const getChannelAliasesTasks = body.channels.map((channel) => () => getAliasForChannel(req.session.selectedNode, channel));
const requestOptions = { ...options };
const getChannelAliasesTasks = body.channels.map((channel) => () => getAliasForChannel(req.session.selectedNode, channel, requestOptions));
common.runWithConcurrencyLimit(getChannelAliasesTasks, 20, () => {
try {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Sorted Channels List Received', data: body });
return res.status(200).json(body);
} catch (e) {
const err = common.handleError(e, 'Channels', 'Get All Channel Aliases Error', req.session.selectedNode);
if (!res.headersSent) { res.status(err.statusCode).json({ message: err.message, error: err.error }); }
logger.log({ selectedNode: req.session.selectedNode, level: 'ERROR', fileName: 'Channels', msg: 'Get All Channel Aliases Error', error: e.message });
if (!res.headersSent) { res.status(500).json({ message: 'Get All Channel Aliases Error', error: e.message }); }
}
});
} else {
@ -68,26 +69,27 @@ export const getPendingChannels = (req, res, next) => {
if (!body.total_limbo_balance) {
body.total_limbo_balance = 0;
}
const requestOptions = { ...options };
const getPendingAliasesTasks = [];
if (body.pending_open_channels && body.pending_open_channels.length > 0) {
body.pending_open_channels?.map((channel) => getPendingAliasesTasks.push(() => getAliasForChannel(req.session.selectedNode, channel.channel)));
body.pending_open_channels?.map((channel) => getPendingAliasesTasks.push(() => getAliasForChannel(req.session.selectedNode, channel.channel, requestOptions)));
}
if (body.pending_force_closing_channels && body.pending_force_closing_channels.length > 0) {
body.pending_force_closing_channels?.map((channel) => getPendingAliasesTasks.push(() => getAliasForChannel(req.session.selectedNode, channel.channel)));
body.pending_force_closing_channels?.map((channel) => getPendingAliasesTasks.push(() => getAliasForChannel(req.session.selectedNode, channel.channel, requestOptions)));
}
if (body.pending_closing_channels && body.pending_closing_channels.length > 0) {
body.pending_closing_channels?.map((channel) => getPendingAliasesTasks.push(() => getAliasForChannel(req.session.selectedNode, channel.channel)));
body.pending_closing_channels?.map((channel) => getPendingAliasesTasks.push(() => getAliasForChannel(req.session.selectedNode, channel.channel, requestOptions)));
}
if (body.waiting_close_channels && body.waiting_close_channels.length > 0) {
body.waiting_close_channels?.map((channel) => getPendingAliasesTasks.push(() => getAliasForChannel(req.session.selectedNode, channel.channel)));
body.waiting_close_channels?.map((channel) => getPendingAliasesTasks.push(() => getAliasForChannel(req.session.selectedNode, channel.channel, requestOptions)));
}
common.runWithConcurrencyLimit(getPendingAliasesTasks, 20, () => {
try {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Pending Channels List Received', data: body });
return res.status(200).json(body);
} catch (e) {
const err = common.handleError(e, 'Channels', 'Get Pending Channel Aliases Error', req.session.selectedNode);
if (!res.headersSent) { res.status(err.statusCode).json({ message: err.message, error: err.error }); }
logger.log({ selectedNode: req.session.selectedNode, level: 'ERROR', fileName: 'Channels', msg: 'Get Pending Channel Aliases Error', error: e.message });
if (!res.headersSent) { res.status(500).json({ message: 'Get Pending Channel Aliases Error', error: e.message }); }
}
});
}).catch((errRes) => {
@ -107,14 +109,15 @@ export const getClosedChannels = (req, res, next) => {
body.channels.forEach((channel) => {
channel.close_type = (!channel.close_type) ? 'COOPERATIVE_CLOSE' : channel.close_type;
});
const getClosedAliasesTasks = body.channels.map((channel) => () => getAliasForChannel(req.session.selectedNode, channel));
const requestOptions = { ...options };
const getClosedAliasesTasks = body.channels.map((channel) => () => getAliasForChannel(req.session.selectedNode, channel, requestOptions));
common.runWithConcurrencyLimit(getClosedAliasesTasks, 20, () => {
try {
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Closed Channels List Received', data: body });
return res.status(200).json(body);
} catch (e) {
const err = common.handleError(e, 'Channels', 'Get Closed Channel Aliases Error', req.session.selectedNode);
if (!res.headersSent) { res.status(err.statusCode).json({ message: err.message, error: err.error }); }
logger.log({ selectedNode: req.session.selectedNode, level: 'ERROR', fileName: 'Channels', msg: 'Get Closed Channel Aliases Error', error: e.message });
if (!res.headersSent) { res.status(500).json({ message: 'Get Closed Channel Aliases Error', error: e.message }); }
}
});
} else {

View file

@ -6,9 +6,9 @@ let options = null;
const logger: LoggerService = Logger;
const common: CommonService = Common;
export const getAliasFromPubkey = (selNode: SelectedNode, pubkey) => {
options.url = selNode.settings.lnServerUrl + '/v1/graph/node/' + pubkey;
return request(options).then((res) => {
export const getAliasFromPubkey = (selNode: SelectedNode, pubkey, requestOptions) => {
requestOptions.url = selNode.settings.lnServerUrl + '/v1/graph/node/' + pubkey;
return request(requestOptions).then((res) => {
logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Graph', msg: 'Alias Received', data: res.node.alias });
return res.node.alias;
}).
@ -80,19 +80,20 @@ export const getQueryRoutes = (req, res, next) => {
request(options).then((body) => {
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Graph', msg: 'Query Routes Received', data: body });
if (body.routes && body.routes.length && body.routes.length > 0 && body.routes[0].hops && body.routes[0].hops.length && body.routes[0].hops.length > 0) {
const getRouteAliasesTasks = body.routes[0].hops.map((hop) => () => getAliasFromPubkey(req.session.selectedNode, hop.pub_key));
const requestOptions = { ...options };
const getRouteAliasesTasks = body.routes[0].hops.map((hop) => () => getAliasFromPubkey(req.session.selectedNode, hop.pub_key, requestOptions));
common.runWithConcurrencyLimit(getRouteAliasesTasks, 20, (values) => {
try {
body.routes[0].hops?.map((hop, i) => {
hop.hop_sequence = i + 1;
hop.pubkey_alias = values[i]?.error ? 'Unknown' : values[i];
hop.pubkey_alias = typeof values[i] === 'string' ? values[i] : 'Unknown';
return hop;
});
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Graph', msg: 'Graph Routes with Alias Received', data: body });
res.status(200).json(body);
} catch (e) {
const err = common.handleError(e, 'Graph', 'Get Query Routes Error', req.session.selectedNode);
if (!res.headersSent) { res.status(err.statusCode).json({ message: err.message, error: err.error }); }
logger.log({ selectedNode: req.session.selectedNode, level: 'ERROR', fileName: 'Graph', msg: 'Get Query Routes Error', error: e.message });
if (!res.headersSent) { res.status(500).json({ message: 'Get Query Routes Error', error: e.message }); }
}
});
} else {
@ -139,15 +140,16 @@ export const getAliasesForPubkeys = (req, res, next) => {
if (options.error) { return res.status(options.statusCode).json({ message: options.message, error: options.error }); }
if (req.query.pubkeys) {
const pubkeyArr = req.query.pubkeys.split(',');
const getAliasesTasks = pubkeyArr.map((pubkey) => () => getAliasFromPubkey(req.session.selectedNode, pubkey));
const requestOptions = { ...options };
const getAliasesTasks = pubkeyArr.map((pubkey) => () => getAliasFromPubkey(req.session.selectedNode, pubkey, requestOptions));
common.runWithConcurrencyLimit(getAliasesTasks, 20, (values) => {
try {
const safeValues = values.map((v) => (v?.error ? 'Unknown' : v));
const safeValues = values.map((v) => (typeof v === 'string' ? v : 'Unknown'));
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Graph', msg: 'Node Alias', data: safeValues });
res.status(200).json(safeValues);
} catch (e) {
const err = common.handleError(e, 'Graph', 'Get Aliases for Pubkeys Error', req.session.selectedNode);
if (!res.headersSent) { res.status(err.statusCode).json({ message: err.message, error: err.error }); }
logger.log({ selectedNode: req.session.selectedNode, level: 'ERROR', fileName: 'Graph', msg: 'Get Aliases for Pubkeys Error', error: e.message });
if (!res.headersSent) { res.status(500).json({ message: 'Get Aliases for Pubkeys Error', error: e.message }); }
}
});
} else {