From 6ec15c10317e0c2c4887a2a960479bd552282c55 Mon Sep 17 00:00:00 2001 From: Alicia Sykes Date: Sat, 12 Sep 2026 13:29:46 +0100 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20No=20need=20to=20buffe?= =?UTF-8?q?r=20unused=20body=20for=20status=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services/endpoints/status-check.js | 1 + services/utils/request.js | 62 +++++++++++++++++------------- 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/services/endpoints/status-check.js b/services/endpoints/status-check.js index 83da3f630c..788899cb69 100644 --- a/services/endpoints/status-check.js +++ b/services/endpoints/status-check.js @@ -40,6 +40,7 @@ const makeRequest = (url, options, render) => { headers, maxRedirects, timeout: 10000, + ignoreResponseBody: true, httpsAgent: { rejectUnauthorized: !enableInsecure }, validateUrl: validateTargetUrl, }) diff --git a/services/utils/request.js b/services/utils/request.js index caef39ae10..d9364616f3 100644 --- a/services/utils/request.js +++ b/services/utils/request.js @@ -100,6 +100,7 @@ function request(config) { maxRedirects = 5, timeout = 0, maxResponseSize = 0, + ignoreResponseBody = false, httpsAgent, validateUrl, } = config; @@ -175,6 +176,39 @@ function request(config) { return; } + // Resolves (or rejects) the promise, from an already-complete response body + const finish = (raw) => { + let responseData; + try { responseData = JSON.parse(raw); } catch (_) { responseData = raw; } + + const response = { + data: responseData, + status: res.statusCode, + statusText: res.statusMessage, + headers: res.headers, + }; + // Expose raw request for socket access (non-enumerable, circular refs break stringify) + Object.defineProperty(response, 'request', { + value: req, + enumerable: false, + }); + + if (res.statusCode >= 200 && res.statusCode < 300) { + resolve(response); + } else { + reject(new RequestError( + `Request failed with status ${res.statusCode}`, + { response, code: res.statusCode }, + )); + } + }; + // If body not needed, can finish early with just status code + if (ignoreResponseBody) { + finish(''); + req.destroy(); + return; + } + // Decompress response based on Content-Encoding (matching axios behavior) let stream = res; const encoding = (res.headers['content-encoding'] || '').toLowerCase(); @@ -209,33 +243,7 @@ function request(config) { }); stream.on('end', () => { if (aborted) return; - const raw = Buffer.concat(chunks).toString('utf8'); - let responseData; - try { responseData = JSON.parse(raw); } catch (_) { responseData = raw; } - - const response = { - data: responseData, - status: res.statusCode, - statusText: res.statusMessage, - headers: res.headers, - }; - // Expose the raw request object for socket access (status-check.js - // needs this). Defined as non-enumerable so JSON.stringify() skips - // it — the http.ClientRequest has circular socket references that - // would otherwise crash any endpoint forwarding the response. - Object.defineProperty(response, 'request', { - value: req, - enumerable: false, - }); - - if (res.statusCode >= 200 && res.statusCode < 300) { - resolve(response); - } else { - reject(new RequestError( - `Request failed with status ${res.statusCode}`, - { response, code: res.statusCode }, - )); - } + finish(Buffer.concat(chunks).toString('utf8')); }); }); From 94e224a9435b83ac969ccdc273ca4406a275e6f4 Mon Sep 17 00:00:00 2001 From: Alicia Sykes Date: Sat, 12 Sep 2026 13:46:31 +0100 Subject: [PATCH 2/3] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Bounds=20user-supplied?= =?UTF-8?q?=20ping=20check=20count=20and=20timeout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services/endpoints/ping-check.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/services/endpoints/ping-check.js b/services/endpoints/ping-check.js index 50f304f21f..006d2f925a 100644 --- a/services/endpoints/ping-check.js +++ b/services/endpoints/ping-check.js @@ -6,6 +6,17 @@ const ping = require('pingman'); const net = require('net'); +// Upper bounds for user-supplied params, matching the documented pingCheck limits +const MAX_COUNT = 5; +const MAX_TIMEOUT = MAX_COUNT * 1000; + +/* Bounds a numeric param, falling back to the default when absent or out of range */ +const boundedParam = (value, fallback, max) => { + const num = Math.floor(Number(value)); + if (!Number.isFinite(num) || num < 1) return fallback; + return Math.min(num, max); +}; + /* Returned if the URL params are not present or correct */ const immediateError = (render, error) => { render(JSON.stringify({ @@ -22,8 +33,8 @@ module.exports = (paramStr, render) => { // Get the url to check from query params const params = new URLSearchParams(paramStr.slice(paramStr.indexOf('?') + 1)); const host = params.get('host') || ''; - const count = Number(params.get('count')) || 2; - const timeout = Number(params.get('timeout')) || 2000; + const count = boundedParam(params.get('count'), 2, MAX_COUNT); + const timeout = boundedParam(params.get('timeout'), 2000, MAX_TIMEOUT); if (!host || typeof host !== 'string') { immediateError(render, 'Invalid host given for ping check.'); return; From 7465221f394842fc1d37c2bed47bcf572cad90d1 Mon Sep 17 00:00:00 2001 From: Alicia Sykes Date: Sat, 12 Sep 2026 14:18:45 +0100 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=90=9B=20Fixes=20sub-second=20ping=20?= =?UTF-8?q?timeouts=20being=20treated=20as=20no=20deadline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services/endpoints/ping-check.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/services/endpoints/ping-check.js b/services/endpoints/ping-check.js index 006d2f925a..65bbf094f9 100644 --- a/services/endpoints/ping-check.js +++ b/services/endpoints/ping-check.js @@ -6,9 +6,8 @@ const ping = require('pingman'); const net = require('net'); -// Upper bounds for user-supplied params, matching the documented pingCheck limits +// Max ICMP packets per check, matching the documented pingCheckCount limit const MAX_COUNT = 5; -const MAX_TIMEOUT = MAX_COUNT * 1000; /* Bounds a numeric param, falling back to the default when absent or out of range */ const boundedParam = (value, fallback, max) => { @@ -34,7 +33,7 @@ module.exports = (paramStr, render) => { const params = new URLSearchParams(paramStr.slice(paramStr.indexOf('?') + 1)); const host = params.get('host') || ''; const count = boundedParam(params.get('count'), 2, MAX_COUNT); - const timeout = boundedParam(params.get('timeout'), 2000, MAX_TIMEOUT); + const timeout = boundedParam(params.get('timeout'), 2000, count * 1000); if (!host || typeof host !== 'string') { immediateError(render, 'Invalid host given for ping check.'); return; @@ -42,7 +41,7 @@ module.exports = (paramStr, render) => { (async () => { try { const configuration = { - timeout: Math.round(timeout/1000), + timeout: Math.max(1, Math.round(timeout / 1000)), numberOfEchos: count, IPV4: net.isIPv4(host), IPV6: net.isIPv6(host),