Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 56 additions & 13 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,24 +199,67 @@ 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 server answering HEAD with 302 and a body, against the HTTP spec. Ignoring.`);
// Proxy fallback configuration
const defaultProxies = [
{ name: 'r.jina.ai', format: (url) => `https://r.jina.ai/${url}` },
{ name: 'corsproxy.io', format: (url) => `https://corsproxy.io/?${encodeURIComponent(url)}` },
{ name: 'allorigins', format: (url) => `https://api.allorigins.win/raw?url=${encodeURIComponent(url)}` },
];
const proxyFallbackEnabled = opts.proxyFallback || opts.fallbackProxies;
const proxies = opts.fallbackProxies || defaultProxies;
const fallbackCodes = opts.fallbackForCodes || [403];

let progressTicked = false;
let callbackCalled = false;

function safeCallback(err, result) {
if (callbackCalled) return;
callbackCalled = true;
callback(err, result);
}

function doLinkCheck(urlToCheck, cb) {
linkCheck(urlToCheck, opts, function (err, result) {
if (opts.showProgressBar && !progressTicked) {
bar.tick();
progressTicked = true;
}

if (err) {
result = new LinkCheckResult(opts, urlToCheck, 500, err);
result.status = 'error';
}

cb(err, result);
});
}

function tryProxies(originalLink, originalResult, proxyIndex) {
if (proxyIndex >= proxies.length) {
safeCallback(null, originalResult);
return;
}
numCalls += 1;

if (opts.showProgressBar) {
bar.tick();
}
const proxy = proxies[proxyIndex];
const proxyUrl = proxy.format(originalLink);

if (err) {
result = new LinkCheckResult(opts, link, 500, err);
result.status = 'error'; // custom status for errored links
}
doLinkCheck(proxyUrl, function (proxyErr, proxyResult) {
if (proxyResult && proxyResult.statusCode >= 200 && proxyResult.statusCode < 400) {
const successResult = new LinkCheckResult(opts, originalLink, 200, undefined);
successResult.proxyUsed = proxy.name;
safeCallback(null, successResult);
} else {
tryProxies(originalLink, originalResult, proxyIndex + 1);
}
});
}

callback(null, result);
doLinkCheck(link, function (err, result) {
if (proxyFallbackEnabled && result && fallbackCodes.includes(result.statusCode)) {
tryProxies(link, result, 0);
} else {
safeCallback(null, result);
}
});
}, callback);
};
4 changes: 4 additions & 0 deletions markdown-link-check
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ function getInputs() {
.option('-i, --ignore <paths>', 'ignore input paths including an ignore path', commaSeparatedPathsList)
.option('-a, --alive <code>', 'comma separated list of HTTP codes to be considered as alive', commaSeparatedCodesList)
.option('-r, --retry', 'retry after the duration indicated in \'retry-after\' header when HTTP code is 429')
.option('--proxy-fallback', 'retry 403 links through proxy services')
.option('--reporters <names>', 'specify reporters to use', commaSeparatedReportersList)
.option('--projectBaseUrl <url>', 'the URL to use for {{BASEURL}} replacement')
.option('--junit-output <file>', 'output file for JUnit XML report (only used with junit reporter)')
Expand Down Expand Up @@ -359,6 +360,7 @@ function getInputs() {
input.opts.quiet = (program.opts().quiet === true);
input.opts.verbose = (program.opts().verbose === true);
input.opts.retryOn429 = (program.opts().retry === true);
input.opts.proxyFallback = (program.opts().proxyFallback === true);
input.opts.aliveStatusCodes = program.opts().alive;
input.opts.reporters = program.opts().reporters ?? [ reporters.default ];
input.opts.junitOutput = program.opts().junitOutput;
Expand Down Expand Up @@ -437,6 +439,8 @@ async function processInput(filenameForOutput, stream, opts) {
opts.fallbackRetryDelay = config.fallbackRetryDelay;
opts.aliveStatusCodes = config.aliveStatusCodes;
opts.reporters = config.reporters ?? opts.reporters;
opts.fallbackProxies = config.fallbackProxies;
opts.fallbackForCodes = config.fallbackForCodes;
}

await runMarkdownLinkCheck(filenameForOutput, markdown, opts);
Expand Down