Skip to content
Merged
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"check:content": "node scripts/check-content.mjs",
"check:site": "node scripts/check-site.mjs",
"check:external": "node scripts/check-external-links.mjs",
"check": "npm run check:content && npm run build && npm run check:site"
"test": "node --test scripts/*.test.mjs",
"check": "npm test && npm run check:content && npm run build && npm run check:site"
},
"dependencies": {
"astro": "^7.2.8"
Expand Down
3 changes: 3 additions & 0 deletions scripts/check-external-links.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ async function check(url) {
headers: { 'user-agent': 'SagaSmithAI-site-link-check/1.0', range: 'bytes=0-0' },
});
}
// Only the status is needed. A server may ignore Range and stream an
// unlimited GET body, keeping connections and the checker alive.
await response.body?.cancel();
if (response.status === 404 || response.status === 410) failures.push(`${response.status} ${url}`);
else if (!response.ok && response.status !== 403 && response.status !== 429) failures.push(`${response.status} ${url}`);
else if (!response.ok) warnings.push(`${response.status} ${url}`);
Expand Down
46 changes: 46 additions & 0 deletions scripts/check-external-links.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
import { once } from 'node:events';
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
import { createServer } from 'node:http';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import test from 'node:test';

test('GET fallback cancels a body when the server ignores Range', { timeout: 10_000 }, async () => {
const root = await mkdtemp(path.join(tmpdir(), 'sagasmith-links-'));
const server = createServer((request, response) => {
if (request.method === 'HEAD') {
response.writeHead(405).end();
return;
}
response.writeHead(200, { 'content-type': 'text/plain' });
response.write('body that continues until cancelled');
const interval = setInterval(() => response.write('.'), 100);
response.on('close', () => clearInterval(interval));
});
let child;
try {
server.listen(0, '127.0.0.1');
await once(server, 'listening');
await mkdir(path.join(root, 'dist'));
await writeFile(path.join(root, 'dist/index.html'),
`<a href="http://127.0.0.1:${server.address().port}/resource">Resource</a>`);
const script = fileURLToPath(new URL('./check-external-links.mjs', import.meta.url));
child = spawn(process.execPath, [script], { cwd: root, stdio: 'pipe' });
let output = '';
child.stdout.on('data', (chunk) => { output += chunk; });
child.stderr.on('data', (chunk) => { output += chunk; });
const timeout = setTimeout(() => child.kill(), 5_000);
const [code] = await once(child, 'close');
clearTimeout(timeout);
assert.equal(code, 0, output);
assert.match(output, /External-link checks passed/);
} finally {
if (child && child.exitCode === null) child.kill();
server.closeAllConnections();
await new Promise((resolve) => server.close(resolve));
await rm(root, { recursive: true, force: true });
}
});