-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathccc-http.js
More file actions
160 lines (135 loc) · 6.12 KB
/
ccc-http.js
File metadata and controls
160 lines (135 loc) · 6.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
const debug = require('debug')('cdn-cache-check-http');
debug('Entry: [%s]', __filename);
//Load configuration library
const config = require('./ccc-configuration');
// Load error handling
const { CccError, CccErrorTypes } = require('./ccc-lib');
// Initialise 'needle' HTTP client
const needle = require('needle');
/**
* Issue HTTP request with rate limiting
* @param {string} url - The URL to request
* @param {string} method - HTTP method
* @param {object} httpOptions - HTTP options
* @returns {Promise} Promise that resolves with the result object
*/
function issueRequest(url, method, httpOptions) {
return new Promise((resolve) => {
// Initialise result object
let result = config.initResponseObject();
// Parse URL into component parts
let requestURL = new URL(url);
// Populate the request properties for reference
result.request.protocol = requestURL.protocol;
result.request.host = requestURL.hostname;
result.request.path = requestURL.pathname;
result.request.url = url;
debug(`Issuing HTTP ${method.toUpperCase()} request to [${url}]...`);
// Send HTTP request for current URL
let resp = needle.request(method, url, '', httpOptions, (error, response) => {
debug('Callback for [%s] received', url);
if (error) {
debug('Error for [%s]: %O', url, error);
// Log error to JSON result
result.error = true;
result.response.headers = [];
// The 'error' object may have different properties depending on the cause (e.g. HTTP error vs network error)
if (Object.prototype.hasOwnProperty.call(error, 'code')) {
result.statusCode = error.code;
} else if (Object.prototype.hasOwnProperty.call(error, 'message')) {
result.statusCode = error.message;
} else {
result.statusCode = 'error';
debug('A response error occurred when requesting [%s] but the specific error code could not be extracted from the error object:', url);
debug(error);
}
} else {
// We got a HTTP response
debug(`${response.statusCode} ${url}`);
// Save request details and HTTP response headers as JSON
result.statusCode = response.statusCode;
result.request.protocol = response.req.protocol;
result.request.host = response.req.host;
result.request.path = response.req.path;
result.response.headers = response.headers;
// Get IP Address from response if it exists
if (typeof (response.socket?.remoteAddress) === 'string') {
result.ipAddress = response.socket.remoteAddress;
}
// Get the IP Family (IPv4 or IPv6) from the response if it exists
if (typeof (response.socket?.remoteFamily) === 'string') {
result.ipFamily = response.socket.remoteFamily;
}
}
resolve(result);
});
resp.on('redirect', (redirectUrl) => {
result.redirectCount += 1;
debug('redirectCount incremented to %s by redirect event to [%s] ', result.redirectCount, redirectUrl);
});
});
}
/**
* Issue HTTP requests to multiple URLs with concurrency limit
* @param {string[]} urls - Array of URLs to request
* @param {object} settings - Settings object with method and HTTP options
* @returns {Promise<Array>} Promise that resolves with array of response objects
*/
let issueRequests = (urls, settings) => {
debug('issueRequests() :: Entry');
return new Promise(function (resolve, reject) {
if ((Array.isArray(urls) && urls.length)) { // Process urls[] array
debug(`The urls[] array has ${urls.length} entries`);
// Set concurrency limit (default: 10, configurable via settings)
const concurrencyLimit = settings.options.httpOptions.concurrency || global.CCC_HTTP_CONCURRENCY_LIMIT || 10;
debug(`Using concurrency limit of ${concurrencyLimit}`);
// Keep array of request/response headers
let responses = [];
let activeRequests = 0;
let currentIndex = 0;
/**
* Process the next URL in the queue
*/
function processNext() {
// Check if we've processed all URLs
if (currentIndex >= urls.length && activeRequests === 0) {
debug(`About to resolve the issueRequests() Promise after ${responses.length} responses out of ${urls.length} requests`);
resolve(responses);
return;
}
// Process URLs while under concurrency limit
while (activeRequests < concurrencyLimit && currentIndex < urls.length) {
const urlIndex = currentIndex;
const url = urls[urlIndex];
currentIndex++;
activeRequests++;
debug(`(${urlIndex + 1} of ${urls.length}) - Starting request [${url}] (active: ${activeRequests})`);
// Issue the HTTP request
issueRequest(url, settings.method, settings.options.httpOptions)
.then((result) => {
responses.push(result);
activeRequests--;
debug('Completed request %s of %s (active: %s)', responses.length, urls.length, activeRequests);
processNext();
})
.catch((error) => {
// This shouldn't happen as issueRequest() always resolves, but handle it just in case
debug('Unexpected error in issueRequest: %O', error);
activeRequests--;
processNext();
});
}
}
// Start processing
processNext();
} else {
// urls[] array does not exist, is not an array, or is empty ⇒ do not attempt to process url array
reject(new CccError(
'URLs array either does not exist, is not an array, or is empty',
CccErrorTypes.VALIDATION,
{ urls }
));
}
});
};
module.exports = { issueRequests };