mirror of
https://github.com/Ride-The-Lightning/RTL.git
synced 2026-08-13 12:33:07 +02:00
The 10-minute timeout added for review feedback would have aborted LND's long-poll subscription streams (/v2/invoices/subscribe and /v2/router/track), which legitimately stay open until an invoice settles or a payment resolves - breaking real-time notifications for any invoice paid more than 10 minutes after creation. The wrapper now honors a per-call options.timeout (0 disables the bound, axios semantics; the 10-minute default still applies everywhere else), and both subscription calls pass timeout: 0. They also copy the options object instead of mutating it: addInvoice hands the session-cached options to subscribeToInvoice, so setting the timeout in place would have leaked an unbounded timeout to every subsequent request for that node (getOptions resets form/body/qs but not timeout). Verified on the regtest fixture: with a websocket client connected as alice's frontend, creating an invoice opens the subscription stream, it survives idle, and paying it from the CLN node delivers the SETTLED event over the websocket in real time. The per-call override was also verified directly (timeout: 1000 aborts a slow upstream with ECONNABORTED; timeout: 0 waits it out). Both API suites (31 read + 12 write checks) re-pass.
90 lines
3.8 KiB
TypeScript
90 lines
3.8 KiB
TypeScript
import axios from 'axios';
|
|
import * as https from 'https';
|
|
|
|
// Drop-in replacement for the deprecated request-promise, backed by axios.
|
|
// Accepts the same options shape used across the controllers ({ url, baseUrl,
|
|
// uri, qs, form, body, headers, rejectUnauthorized, json }), resolves with the
|
|
// response body directly and rejects with a plain, serializable object that
|
|
// mirrors request-promise's StatusCodeError/RequestError shape expected by
|
|
// CommonService.handleError. Auth headers are intentionally excluded from the
|
|
// rejected error so they can never leak into logs or API error responses.
|
|
|
|
const insecureAgent = new https.Agent({ rejectUnauthorized: false });
|
|
|
|
const buildConfig = (options, method: string) => {
|
|
const config: any = {
|
|
url: options.url && options.url !== '' ? options.url : options.uri,
|
|
method: method || options.method || 'GET',
|
|
headers: options.headers ? { ...options.headers } : {},
|
|
// Bound hung upstreams; 10 minutes accommodates the slowest legitimate
|
|
// operations (LND's /v2/router/send streams up to timeout_seconds=600 and
|
|
// slow CLN channel operations get req.setTimeout(600000) upstream).
|
|
// Callers can override per request; 0 disables the bound entirely, which
|
|
// the LND invoice/payment subscription streams need (open until settled).
|
|
timeout: options.timeout !== null && options.timeout !== undefined ? options.timeout : 600000
|
|
};
|
|
if (options.baseUrl) { config.baseURL = options.baseUrl; }
|
|
if (options.qs && Object.keys(options.qs).length > 0) { config.params = options.qs; }
|
|
if (options.rejectUnauthorized === false) { config.httpsAgent = insecureAgent; }
|
|
if (options.form !== null && options.form !== undefined) {
|
|
if (typeof options.form === 'string') {
|
|
// Pre-encoded (or raw JSON string for LND's wallet endpoints), send as-is.
|
|
config.data = options.form;
|
|
} else {
|
|
const params = new URLSearchParams();
|
|
Object.entries(options.form).forEach(([key, value]) => {
|
|
if (value === null || value === undefined) { return; }
|
|
if (Array.isArray(value)) {
|
|
// Eclair parses list fields as comma-separated values; omit empty lists
|
|
// (request-promise's qs encoding also dropped them).
|
|
if (value.length > 0) { params.append(key, value.join(',')); }
|
|
} else {
|
|
params.append(key, String(value));
|
|
}
|
|
});
|
|
config.data = params;
|
|
}
|
|
config.headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
|
} else if (options.body !== null && options.body !== undefined) {
|
|
config.data = options.body;
|
|
}
|
|
if (options.json !== true) {
|
|
// Callers without json: true (block explorer, currency rates) JSON.parse the body themselves.
|
|
config.responseType = 'text';
|
|
config.transformResponse = [(data) => data];
|
|
}
|
|
return config;
|
|
};
|
|
|
|
const toRequestPromiseError = (err, config) => {
|
|
const errOptions = { url: config.url, method: config.method };
|
|
if (err.response) {
|
|
return {
|
|
name: 'StatusCodeError',
|
|
statusCode: err.response.status,
|
|
message: err.response.status + ' - ' + JSON.stringify(err.response.data),
|
|
error: err.response.data,
|
|
options: errOptions
|
|
};
|
|
}
|
|
const message = err.message && err.message !== '' ? err.message : err.code;
|
|
return {
|
|
name: 'RequestError',
|
|
message: message,
|
|
error: { code: err.code, message: message },
|
|
options: errOptions
|
|
};
|
|
};
|
|
|
|
const call = (options, method?: string) => {
|
|
const config = buildConfig(options, method);
|
|
return axios.request(config).then((response) => response.data).catch((err) => Promise.reject(toRequestPromiseError(err, config)));
|
|
};
|
|
|
|
const request = (options) => call(options);
|
|
request.get = (options) => call(options, 'GET');
|
|
request.post = (options) => call(options, 'POST');
|
|
request.put = (options) => call(options, 'PUT');
|
|
request.delete = (options) => call(options, 'DELETE');
|
|
|
|
export default request;
|