Skip to content

Commit cf29469

Browse files
authoredDec 24, 2020
feat: expose error details when verbosity=DEBUG (#215)
1 parent d20cff5 commit cf29469

File tree

4 files changed

+49
-20
lines changed

4 files changed

+49
-20
lines changed
 

‎src/cli.ts

+1
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ async function main() {
190190
case LinkState.BROKEN:
191191
state = `[${chalk.red(link.status!.toString())}]`;
192192
logger.error(` ${state} ${chalk.gray(link.url)}`);
193+
logger.debug(JSON.stringify(link.failureDetails, null, 2));
193194
break;
194195
case LinkState.OK:
195196
state = `[${chalk.green(link.status!.toString())}]`;

‎src/index.ts

+13-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {EventEmitter} from 'events';
2-
import * as gaxios from 'gaxios';
2+
import {request, GaxiosResponse} from 'gaxios';
33
import * as http from 'http';
44
import enableDestroy = require('server-destroy');
55
import finalhandler = require('finalhandler');
@@ -41,6 +41,7 @@ export interface LinkResult {
4141
status?: number;
4242
state: LinkState;
4343
parent?: string;
44+
failureDetails?: {}[];
4445
}
4546

4647
export interface CrawlResult {
@@ -319,9 +320,10 @@ export class LinkChecker extends EventEmitter {
319320
let state = LinkState.BROKEN;
320321
let data = '';
321322
let shouldRecurse = false;
322-
let res: gaxios.GaxiosResponse<string> | undefined = undefined;
323+
let res: GaxiosResponse<string> | undefined = undefined;
324+
const failures: {}[] = [];
323325
try {
324-
res = await gaxios.request<string>({
326+
res = await request<string>({
325327
method: opts.crawl ? 'GET' : 'HEAD',
326328
url: opts.url.href,
327329
headers,
@@ -332,7 +334,7 @@ export class LinkChecker extends EventEmitter {
332334

333335
// If we got an HTTP 405, the server may not like HEAD. GET instead!
334336
if (res.status === 405) {
335-
res = await gaxios.request<string>({
337+
res = await request<string>({
336338
method: 'GET',
337339
url: opts.url.href,
338340
headers,
@@ -345,6 +347,7 @@ export class LinkChecker extends EventEmitter {
345347
// request failure: invalid domain name, etc.
346348
// this also occasionally catches too many redirects, but is still valid (e.g. https://www.ebay.com)
347349
// for this reason, we also try doing a GET below to see if the link is valid
350+
failures.push(err);
348351
}
349352

350353
try {
@@ -353,7 +356,7 @@ export class LinkChecker extends EventEmitter {
353356
(res === undefined || res.status < 200 || res.status >= 300) &&
354357
!opts.crawl
355358
) {
356-
res = await gaxios.request<string>({
359+
res = await request<string>({
357360
method: 'GET',
358361
url: opts.url.href,
359362
responseType: 'text',
@@ -363,6 +366,7 @@ export class LinkChecker extends EventEmitter {
363366
});
364367
}
365368
} catch (ex) {
369+
failures.push(ex);
366370
// catch the next failure
367371
}
368372

@@ -375,13 +379,16 @@ export class LinkChecker extends EventEmitter {
375379
// Assume any 2xx status is 👌
376380
if (status >= 200 && status < 300) {
377381
state = LinkState.OK;
382+
} else {
383+
failures.push(res!);
378384
}
379385

380386
const result: LinkResult = {
381387
url: opts.url.href,
382388
status,
383389
state,
384390
parent: opts.parent,
391+
failureDetails: failures,
385392
};
386393
opts.results.push(result);
387394
this.emit('link', result);
@@ -455,7 +462,7 @@ export async function check(options: CheckOptions) {
455462
* @param {object} response Page response.
456463
* @returns {boolean}
457464
*/
458-
function isHtml(response: gaxios.GaxiosResponse): boolean {
465+
function isHtml(response: GaxiosResponse): boolean {
459466
const contentType = response.headers['content-type'] || '';
460467
return (
461468
!!contentType.match(/text\/html/g) ||

‎test/test.ts

+9
Original file line numberDiff line numberDiff line change
@@ -436,4 +436,13 @@ describe('linkinator', () => {
436436
assert.ok(results.passed);
437437
scopes.forEach(x => x.done());
438438
});
439+
440+
it('should surface call stacks on failures in the API', async () => {
441+
const results = await check({
442+
path: 'http://fake.local',
443+
});
444+
assert.ok(!results.passed);
445+
const err = results.links[0].failureDetails![0] as Error;
446+
assert.ok(/Nock: Disallowed net connect for/.test(err.message));
447+
});
439448
});

‎test/zcli.ts

+26-14
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ describe('cli', () => {
1111
const res = await execa('npx', ['linkinator', 'test/fixtures/basic'], {
1212
reject: false,
1313
});
14-
assert.include(res.stderr, 'ERROR: Detected 1 broken links');
14+
assert.match(res.stderr, /ERROR: Detected 1 broken links/);
1515
});
1616

1717
it('should pass successful markdown scan', async () => {
1818
const res = await execa('npx', [
1919
'linkinator',
2020
'test/fixtures/markdown/README.md',
2121
]);
22-
assert.include(res.stderr, 'Successfully scanned');
22+
assert.match(res.stderr, /Successfully scanned/);
2323
});
2424

2525
it('should allow multiple paths', async () => {
@@ -28,14 +28,14 @@ describe('cli', () => {
2828
'test/fixtures/markdown/unlinked.md',
2929
'test/fixtures/markdown/README.md',
3030
]);
31-
assert.include(res.stderr, 'Successfully scanned');
31+
assert.match(res.stderr, /Successfully scanned/);
3232
});
3333

3434
it('should show help if no params are provided', async () => {
3535
const res = await execa('npx', ['linkinator'], {
3636
reject: false,
3737
});
38-
assert.include(res.stdout, '$ linkinator LOCATION [ --arguments ]');
38+
assert.match(res.stdout, /\$ linkinator LOCATION \[ --arguments \]/);
3939
});
4040

4141
it('should flag skipped links', async () => {
@@ -47,7 +47,7 @@ describe('cli', () => {
4747
'LICENSE.md',
4848
'test/fixtures/markdown/README.md',
4949
]);
50-
assert.include(res.stdout, '[SKP]');
50+
assert.match(res.stdout, /\[SKP\]/);
5151
});
5252

5353
it('should provide CSV if asked nicely', async () => {
@@ -58,7 +58,7 @@ describe('cli', () => {
5858
'csv',
5959
'test/fixtures/markdown/README.md',
6060
]);
61-
assert.include(res.stdout, '/README.md,200,OK,');
61+
assert.match(res.stdout, /\/README.md,200,OK,/);
6262
});
6363

6464
it('should provide JSON if asked nicely', async () => {
@@ -69,7 +69,7 @@ describe('cli', () => {
6969
'json',
7070
'test/fixtures/markdown/README.md',
7171
]);
72-
assert.include(res.stdout, '{');
72+
assert.match(res.stdout, /{/);
7373
});
7474

7575
it('should not show links if --silent', async () => {
@@ -78,7 +78,7 @@ describe('cli', () => {
7878
'--silent',
7979
'test/fixtures/markdown/README.md',
8080
]);
81-
assert.strictEqual(res.stdout.indexOf('['), -1);
81+
assert.notMatch(res.stdout, /\[/);
8282
});
8383

8484
it('should accept a server-root', async () => {
@@ -89,7 +89,7 @@ describe('cli', () => {
8989
'test/fixtures/markdown',
9090
'README.md',
9191
]);
92-
assert.ok(res.stderr.includes('Successfully scanned'));
92+
assert.match(res.stderr, /Successfully scanned/);
9393
});
9494

9595
it('should accept globs', async () => {
@@ -98,7 +98,7 @@ describe('cli', () => {
9898
'test/fixtures/markdown/*.md',
9999
'test/fixtures/markdown/**/*.md',
100100
]);
101-
assert.ok(res.stderr.includes('Successfully scanned'));
101+
assert.match(res.stderr, /Successfully scanned/);
102102
});
103103

104104
it('should throw on invalid format', async () => {
@@ -109,7 +109,7 @@ describe('cli', () => {
109109
reject: false,
110110
}
111111
);
112-
assert.include(res.stderr, 'FORMAT must be');
112+
assert.match(res.stderr, /FORMAT must be/);
113113
});
114114

115115
it('should throw on invalid format', async () => {
@@ -120,7 +120,7 @@ describe('cli', () => {
120120
reject: false,
121121
}
122122
);
123-
assert.include(res.stderr, 'FORMAT must be');
123+
assert.match(res.stderr, /FORMAT must be/);
124124
});
125125

126126
it('should throw on invalid verbosity', async () => {
@@ -131,7 +131,7 @@ describe('cli', () => {
131131
reject: false,
132132
}
133133
);
134-
assert.include(res.stderr, 'VERBOSITY must be');
134+
assert.match(res.stderr, /VERBOSITY must be/);
135135
});
136136

137137
it('should throw when verbosity and silent are flagged', async () => {
@@ -142,7 +142,7 @@ describe('cli', () => {
142142
reject: false,
143143
}
144144
);
145-
assert.include(res.stderr, 'The SILENT and VERBOSITY flags');
145+
assert.match(res.stderr, /The SILENT and VERBOSITY flags/);
146146
});
147147

148148
it('should show no output for verbosity=NONE', async () => {
@@ -157,4 +157,16 @@ describe('cli', () => {
157157
assert.strictEqual(res.stdout, '');
158158
assert.strictEqual(res.stderr, '');
159159
});
160+
161+
it('should show callstacks for verbosity=DEBUG', async () => {
162+
const res = await execa(
163+
'npx',
164+
['linkinator', 'test/fixtures/basic', '--verbosity', 'DEBUG'],
165+
{
166+
reject: false,
167+
}
168+
);
169+
assert.strictEqual(res.exitCode, 1);
170+
assert.match(res.stdout, /reason: getaddrinfo/);
171+
});
160172
});

0 commit comments

Comments
 (0)