From 2b11cd7481e5fc7550e6c098a948a6ab8cf85ef8 Mon Sep 17 00:00:00 2001 From: Cristian Klein Date: Mon, 17 Nov 2025 17:33:07 +0100 Subject: [PATCH 1/4] Fix: Error: Callback was already called In certain odd cases, like a website returning a 302 and/or closing the connection after a HEAD (I'm not 100% sure), linkCheck will call our callback twice. `async` is not amused and will throw: > Error: Callback was already called A campaign of ad-hoc `console.log()`s suggest this is a bug in needle, a library used by link-check. Fixes #222 and #473 --- index.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/index.js b/index.js index 167e9d47..106ca212 100644 --- a/index.js +++ b/index.js @@ -199,7 +199,14 @@ module.exports = function markdownLinkCheck(markdown, opts, callback) { return; } + let numCalls = 0; linkCheck(link, opts, function (err, result) { + if (numCalls > 0) { + console.trace(`linkCheck called us back more than once for ${link}. This is likely due to a redirect (301, 302 or 303). Ignoring.`); + return; + } + numCalls += 1; + if (opts.showProgressBar) { bar.tick(); } From 9a288d62c2ab34e7d9953d24a90e6e5a53ea54d2 Mon Sep 17 00:00:00 2001 From: Cristian Klein Date: Tue, 18 Nov 2025 11:47:28 +0100 Subject: [PATCH 2/4] Add editorconfig --- .editorconfig | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..3eda792a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 4 From 599ffd887e05407c5789903251e709fea2e9a5e0 Mon Sep 17 00:00:00 2001 From: Cristian Klein Date: Tue, 18 Nov 2025 11:52:20 +0100 Subject: [PATCH 3/4] Add test case --- test/markdown-link-check.test.js | 19 +++++++++++++++++++ test/sample.md | 1 + 2 files changed, 20 insertions(+) diff --git a/test/markdown-link-check.test.js b/test/markdown-link-check.test.js index a2a72c11..10572bf0 100644 --- a/test/markdown-link-check.test.js +++ b/test/markdown-link-check.test.js @@ -53,6 +53,22 @@ describe('markdown-link-check', function () { res.json({foo:'bar'}); }); + app.use('/redirect-with-body-in-head', function (req, res) { + /* Don't judge me. I found at least one server on the + * Internet doing this and it triggered a bug. */ + const body = 'Let me send you a body, although you only asked for a HEAD.'; + + const headers = + 'HTTP/1.1 302 Found\r\n' + + 'Connection: close\r\n' + + 'Location: /foo/bar\r\n' + + `Content-Length: ${Buffer.byteLength(body)}\r\n` + + '\r\n'; + + res.socket.write(headers + body); + res.socket.destroy(); + }); + app.get('/basic-auth', function (req, res) { if (req.headers["authorization"] === "Basic Zm9vOmJhcg==") { res.sendStatus(200); @@ -110,6 +126,9 @@ describe('markdown-link-check', function () { expect(results).to.be.an('array'); const expected = [ + // redirect-with-body-in-head + { statusCode: 200, status: 'alive' }, + // redirect-loop { statusCode: 0, status: 'dead' }, diff --git a/test/sample.md b/test/sample.md index 830803fd..42f13e80 100644 --- a/test/sample.md +++ b/test/sample.md @@ -2,6 +2,7 @@ This is a test file: +* [redirect-with-body-in-head](%%BASE_URL%%/redirect-with-body-in-head) (alive) * [redirect-loop](%%BASE_URL%%/loop) (dead) * [valid](%%BASE_URL%%/foo/bar) (alive) * [invalid](%%BASE_URL%%/foo/dead) (dead) From a2f372558f2bfee9f1379a374cb1a739d0e0f2f5 Mon Sep 17 00:00:00 2001 From: Cristian Klein Date: Tue, 18 Nov 2025 11:54:32 +0100 Subject: [PATCH 4/4] Clarify warning message --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 106ca212..032903fc 100644 --- a/index.js +++ b/index.js @@ -202,7 +202,7 @@ module.exports = function markdownLinkCheck(markdown, opts, callback) { let numCalls = 0; linkCheck(link, opts, function (err, result) { if (numCalls > 0) { - console.trace(`linkCheck called us back more than once for ${link}. This is likely due to a redirect (301, 302 or 303). Ignoring.`); + console.trace(`linkCheck called us back more than once for ${link}. This is likely due to a server answering HEAD with 302 and a body, against the HTTP spec. Ignoring.`); return; } numCalls += 1;