From 0e837c102a60b876489353e97df84200c94073e3 Mon Sep 17 00:00:00 2001 From: Evan Hahn Date: Mon, 3 Aug 2026 14:40:38 -0500 Subject: [PATCH] Allowed localhost when making requests ref https://github.com/TryGhost/Ghost/pull/28978#pullrequestreview-4802407471 Post scheduling is broken in development because we use a `localhost` URL. `@tryghost/request` doesn't consider `localhost` URLs valid, so scheduling is broken. This fixes that by allowing `localhost` URLs. I was a little nervous about loosening the validation, but it's code could make requests to `localhost` before this change. (I considered an alternative where Ghost development used `127.0.0.1` instead, which also fixes the problem, but that's too disruptive to development IMO.) --- packages/request/lib/request.js | 6 +++++- packages/request/test/request.test.js | 29 +++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/packages/request/lib/request.js b/packages/request/lib/request.js index c254eae94..e53555cb1 100644 --- a/packages/request/lib/request.js +++ b/packages/request/lib/request.js @@ -34,7 +34,11 @@ module.exports = async function request(url, options = {}) { // DNS cache lookup has already been configured. } - if (_.isEmpty(url) || !validator.isURL(url)) { + const isUrlValid = + typeof url === 'string' && + // `validator.isURL` doesn't let us express "any TLD or localhost", so we do two checks. + (validator.isURL(url) || validator.isURL(url, { host_whitelist: ['localhost'] })); + if (!isUrlValid) { return Promise.reject( new errors.InternalServerError({ message: 'URL empty or invalid.', diff --git a/packages/request/test/request.test.js b/packages/request/test/request.test.js index 957f1be35..eaf5f5874 100644 --- a/packages/request/test/request.test.js +++ b/packages/request/test/request.test.js @@ -120,6 +120,35 @@ describe('Request', function () { ); }); + it('[failure] rejects URL longer than 2084 characters', function () { + const url = `http://example.com/${'a'.repeat(2066)}`; + + assert.equal(url.length, 2085); + + return assert.rejects(request(url), { + message: 'URL empty or invalid.', + }); + }); + + ['http://example.com/white space', 'http://example.com/'].forEach((url) => { + it(`[failure] rejects URL containing invalid characters: ${url}`, function () { + return assert.rejects(request(url), { + message: 'URL empty or invalid.', + }); + }); + }); + + it('[success] allows localhost URL', function () { + const url = 'http://localhost:2368/endpoint/'; + const requestMock = nock('http://localhost:2368') + .get('/endpoint/') + .reply(200, 'Response body'); + + return request(url).then(function () { + assert.equal(requestMock.isDone(), true); + }); + }); + it('[failure] can handle empty url', function () { const url = ''; const options = {