Skip to content

Commit a8c0473

Browse files
authored
Merge pull request #124 from Munter/fix/preconnect
Fix checking of <link rel=preconnect href=...>
2 parents 576a34e + aaa4fce commit a8c0473

File tree

1 file changed

+120
-42
lines changed

1 file changed

+120
-42
lines changed

lib/index.js

Lines changed: 120 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ var request = require('request');
66
var version = require('../package.json').version;
77
var relationDebugDescription = require('./relationDebugDescription');
88
var prettyBytes = require('pretty-bytes');
9+
var urlModule = require('url');
10+
var net = require('net');
11+
var tls = require('tls');
912

1013
var checkFragments = require('./transforms/checkFragments');
1114

@@ -40,7 +43,9 @@ module.exports = function (options) {
4043
return false;
4144
}
4245

43-
var relationTypeExclusions = [];
46+
var relationTypeExclusions = [
47+
'HtmlPreconnectLink'
48+
];
4449

4550
if (!options.recursive) {
4651
relationTypeExclusions.push('HtmlAnchor');
@@ -55,67 +60,77 @@ module.exports = function (options) {
5560
crossorigin: false
5661
};
5762

58-
function logHttpResult(status, url, redirects, relations) {
63+
function logResult(status, url, redirects, relations) {
5964
redirects = redirects || [];
6065
relations = relations || [];
6166

6267
var at = _.uniq(relations.map(relationDebugDescription)).join('\n ');
6368
var skip = shouldSkip(url);
6469

65-
if (status !== 200) {
66-
var invalidStatusReport = {
70+
if (status === false) {
71+
errorCount += 1;
72+
t.push(null, {
73+
ok: false,
74+
skip: skip,
75+
name: 'should accept connections',
76+
operator: 'error',
77+
expected: ['connection accepted', url].join(' '),
78+
actual: [status, url].join(' '),
79+
at: at
80+
});
81+
} else if (status !== 200 && status !== true) {
82+
errorCount += 1;
83+
t.push(null, {
6784
ok: false,
6885
skip: skip,
6986
name: 'should respond with HTTP status 200',
7087
operator: 'error',
7188
expected: [200, url].join(' '),
7289
actual: [status, url].join(' '),
7390
at: at
74-
};
75-
76-
errorCount += 1;
77-
t.push(null, invalidStatusReport);
91+
});
7892
}
7993

80-
var report = {
81-
ok: true,
82-
skip: skip,
83-
name: 'URI should have no redirects - ' + url,
84-
operator: 'noRedirects',
85-
expected: [200, url].join(' '),
86-
at: at
87-
};
94+
if (typeof status !== 'boolean') {
95+
var report = {
96+
ok: true,
97+
skip: skip,
98+
name: 'URI should have no redirects - ' + url,
99+
operator: 'noRedirects',
100+
expected: [200, url].join(' '),
101+
at: at
102+
};
88103

89-
if (redirects.length) {
90-
var log = [].concat({ redirectUri: url }, redirects).map(function (item, idx, arr) {
91-
if (arr[idx + 1]) {
92-
item.statusCode = arr[idx + 1].statusCode;
93-
} else {
94-
item.statusCode = 200;
95-
}
104+
if (redirects.length) {
105+
var log = [].concat({ redirectUri: url }, redirects).map(function (item, idx, arr) {
106+
if (arr[idx + 1]) {
107+
item.statusCode = arr[idx + 1].statusCode;
108+
} else {
109+
item.statusCode = 200;
110+
}
96111

97-
return item;
98-
});
112+
return item;
113+
});
99114

100-
var logLine = log.map(function (redirect) {
101-
return [redirect.statusCode, redirect.redirectUri].join(' ');
102-
}).join(' --> ');
115+
var logLine = log.map(function (redirect) {
116+
return [redirect.statusCode, redirect.redirectUri].join(' ');
117+
}).join(' --> ');
103118

104-
report.actual = logLine;
119+
report.actual = logLine;
105120

106-
if (log[0].statusCode !== 302) {
107-
report.ok = false;
121+
if (log[0].statusCode !== 302) {
122+
report.ok = false;
123+
}
124+
} else {
125+
report.actual = [status, url].join(' ');
108126
}
109-
} else {
110-
report.actual = [status, url].join(' ');
111-
}
112127

113-
if (!report.ok) {
114-
errorCount += 1;
128+
if (!report.ok) {
129+
errorCount += 1;
130+
}
131+
t.push(null, report);
115132
}
116133

117-
t.push(null, report);
118-
119134
// Check for mixed-content warnings
120135
var secureSourceRelations = relations.filter(function (relation) {
121136
return relation.type !== 'HtmlAnchor' && relation.from.nonInlineAncestor.url.indexOf('https:') === 0;
@@ -156,6 +171,42 @@ module.exports = function (options) {
156171
}
157172
}
158173

174+
function tryConnect(url, relations, attempt) {
175+
var urlObj = urlModule.parse(url);
176+
var hostname = urlObj.hostname;
177+
var isTls = urlObj.protocol === 'https:';
178+
var port = urlObj.port ? parseInt(urlObj.port, 10) : (isTls ? 443 : 80);
179+
attempt = attempt || 1;
180+
return function (callback) {
181+
(isTls ? tls : net).connect(port, hostname, function () {
182+
logResult(true, url, undefined, relations);
183+
184+
callback(undefined, true);
185+
}).on('error', function (error) {
186+
var code = error.code;
187+
var status = false;
188+
if (code) {
189+
// Some servers send responses that request apparently handles badly when using the HEAD method...
190+
if (code === 'HPE_INVALID_CONSTANT' && attempt === 1) {
191+
return tryConnect(url, relations, attempt + 1)(callback);
192+
}
193+
194+
if (code === 'ENOTFOUND') {
195+
status = 'DNS Missing';
196+
} else {
197+
status = code;
198+
}
199+
} else {
200+
status = 'Unknown error';
201+
}
202+
203+
logResult(status, url, undefined, relations);
204+
205+
callback(undefined, false);
206+
});
207+
};
208+
}
209+
159210
function httpStatus(url, relations, attempt) {
160211
attempt = attempt || 1;
161212

@@ -192,7 +243,7 @@ module.exports = function (options) {
192243
status = 'Unknown error';
193244
}
194245

195-
logHttpResult(status, url, undefined, relations);
246+
logResult(status, url, undefined, relations);
196247

197248
callback(undefined, status);
198249
} else {
@@ -213,7 +264,7 @@ module.exports = function (options) {
213264
redirects = res.request.redirects || [];
214265
var firstRedirectStatus = redirects[0] && redirects[0].statusCode;
215266

216-
logHttpResult(status, url, redirects, relations);
267+
logResult(status, url, redirects, relations);
217268

218269
callback(undefined, firstRedirectStatus || status);
219270
}
@@ -316,12 +367,13 @@ module.exports = function (options) {
316367
concurrency: options.concurrency || 100
317368
})
318369
.queue(checkFragments(t))
319-
.queue(function (assetGraph, callback) {
370+
.queue(function checkUrls(assetGraph, callback) {
320371
var hrefMap = {};
321372

322373
hrefMap = _.groupBy(assetGraph.findRelations({
323374
crossorigin: true,
324-
href: /^(?:https?:)?\/\//
375+
href: /^(?:https?:)?\/\//,
376+
type: query.not('HtmlPreconnectLink')
325377
}, true), function (relation) {
326378
var url = relation.to.url.replace(/#.*$/, '');
327379

@@ -346,6 +398,32 @@ module.exports = function (options) {
346398
}
347399
);
348400
})
401+
.queue(function checkPreconnects(assetGraph, callback) {
402+
var hrefMap = {};
403+
404+
hrefMap = _.groupBy(assetGraph.findRelations({
405+
crossorigin: true,
406+
href: /^(?:https?:)?\/\//,
407+
type: 'HtmlPreconnectLink'
408+
}, true), function (relation) {
409+
var url = relation.to.url.replace(/#.*$/, '');
410+
411+
return url;
412+
});
413+
414+
var hrefs = Object.keys(hrefMap);
415+
416+
t.push({
417+
name: 'Connecting to ' + hrefs.length + ' hosts (checking <link rel="preconnect" href="...">'
418+
});
419+
async.parallelLimit(
420+
hrefs.map(function (url) {
421+
return tryConnect(url, hrefMap[url]);
422+
}),
423+
20,
424+
callback
425+
);
426+
})
349427
// .writeStatsToStderr()
350428
.run(function () {
351429
var results = t.close();

0 commit comments

Comments
 (0)