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
18 changes: 15 additions & 3 deletions src/crawler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,10 +186,22 @@ export class LinkChecker extends EventEmitter {

const state =
status >= 200 && status < 300 ? LinkState.OK : LinkState.BROKEN;
this.emitResult(opts, state, status, failures);

// Recurse if body is HTML and crawling is enabled
await this.maybeRecurse(opts, response);
if (state === LinkState.BROKEN) {
this.emitResult(opts, state, status, failures);
return;
}

try {
// Recurse if body is HTML and crawling is enabled
await this.maybeRecurse(opts, response);
this.emitResult(opts, state, status, failures);
} catch (error) {
// Report as a broken link when parsing body failed
this.emitResult(opts, LinkState.BROKEN, 0, [
{ cause: (error as Error).cause, message: (error as Error).message },
]);
}
}

// Perform fetch, handle retry on 429, collect failures
Expand Down
12 changes: 8 additions & 4 deletions src/links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,10 +183,14 @@ export async function getLinks(
},
});
await new Promise((resolve, reject) => {
Stream.Readable.fromWeb(source as import('stream/web').ReadableStream)
.pipe(parser)
.on('finish', resolve)
.on('error', reject);
const rs = Stream.Readable.fromWeb(
source as import('stream/web').ReadableStream,
);

// Reject on Readable error
rs.on('error', reject);

rs.pipe(parser).on('finish', resolve).on('error', reject);
});
return links;
}
Expand Down
20 changes: 20 additions & 0 deletions test/test.index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
LinkState,
check,
} from '../src/index.js';
import * as linksMethods from '../src/links.ts';
import { DEFAULT_OPTIONS } from '../src/options.ts';
import { invertedPromise } from './utils.ts';

Expand Down Expand Up @@ -629,6 +630,25 @@ describe('linkinator', () => {
scope.done();
});

it('should treat link as broken when getLinks throws', async () => {
const parseErr = new Error('Parsing failure');
const spy = vi.spyOn(linksMethods, 'getLinks').mockRejectedValue(parseErr);

const checker = new LinkChecker();
const results = await checker.check({
path: 'test/fixtures/basic',
});

assert.ok(!results.passed);
assert.strictEqual(results.links[0].state, LinkState.BROKEN);
assert.strictEqual(
(results.links[0]?.failureDetails?.[0] as Error).message,
'Parsing failure',
);

spy.mockRestore();
});

describe('element metadata', () => {
it('should provide <a> text in results', async () => {
const scope = nock('http://example.invalid').head('/').reply(404);
Expand Down
22 changes: 22 additions & 0 deletions test/test.links.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, expect, it } from 'vitest';
import { getLinks } from '../src/links.js';

describe('getLinks', () => {
it('should reject when the HTML stream emits an error', async () => {
const body = new ReadableStream({
start(controller) {
setTimeout(() => controller.error(new Error('StreamError')), 0);
},
});

const response = {
body,
headers: new Headers({ 'content-type': 'text/html' }),
} as unknown as Response;

// Expect getLinks to reject with our error,
await expect(getLinks(response, 'http://example.invalid')).rejects.toThrow(
'StreamError',
);
});
});
Loading