request-libcurl
Advanced tools
Comparing version 1.0.3 to 2.0.0
### I'm having an issue: | ||
1. Search [issues](https://github.com/VeliovGroup/request-extra/issues), maybe your issue is already solved | ||
2. Before submitting an issue make sure it's only related to `request-extra` package | ||
2. Before submitting an issue make sure it's only related to `request-libcurl` package | ||
3. If your issue is not solved: | ||
- Give an expressive description of what is went wrong | ||
- Version of `request-extra` you're experiencing this issue | ||
- Version of `request-libcurl` you're experiencing this issue | ||
- Version of `Node.js`, `NPM` and `MongoDB` you're experiencing this issue | ||
@@ -10,0 +10,0 @@ - If you're getting an error (exception), please provide full error log (output in console) as file or screenshot |
456
index.js
@@ -6,17 +6,75 @@ 'use strict'; | ||
const request = function LibCurlRequest (_opts, cb) { | ||
const opts = Object.assign({}, request.defaultOptions, _opts); | ||
opts.method = opts.method.toUpperCase(); | ||
let sent = false; | ||
const badUrlError = { | ||
code: 3, | ||
status: 400, | ||
message: '400: URL Malformed / Invalid URL', | ||
errorCode: 3, | ||
statusCode: 400 | ||
}; | ||
const badRequestError = { | ||
code: 43, | ||
status: 400, | ||
message: '400: Bad request', | ||
errorCode: 43, | ||
statusCode: 400 | ||
}; | ||
const abortError = { | ||
code: 42, | ||
status: 408, | ||
message: '408: Request aborted (timeout)', | ||
errorCode: 42, | ||
statusCode: 408 | ||
}; | ||
const noop = () => {}; | ||
const _debug = (...args) => { | ||
console.info.call(console, '[DEBUG] [request-libcurl]', ...args); | ||
}; | ||
const closeCurl = (curl) => { | ||
try { | ||
if (curl && curl.close) { | ||
curl.close.call(curl); | ||
} | ||
} catch (err) { | ||
// we are good here | ||
} | ||
}; | ||
const sendRequest = (libcurl, url, cb) => { | ||
libcurl._debug('[sendRequest]', url.href); | ||
closeCurl(libcurl.curl); | ||
const opts = libcurl.opts; | ||
const curl = new Curl(); | ||
let finished = false; | ||
let retryTimer = null; | ||
let timeoutTimer = null; | ||
let reject = () => {}; | ||
const curl = new Curl(); | ||
const url = new URL(opts.uri); | ||
let isJsonUpload = false; | ||
let hasContentType = false; | ||
let hasContentLength = false; | ||
let hasAcceptEncoding = false; | ||
if (opts.noStorage || opts.rawBody) { | ||
curl.enable(CurlFeature.NO_STORAGE); | ||
const stopRequestTimeout = () => { | ||
if (timeoutTimer) { | ||
clearTimeout(timeoutTimer); | ||
timeoutTimer = null; | ||
} | ||
}; | ||
timeoutTimer = setTimeout(() => { | ||
libcurl.abort(); | ||
}, opts.timeout + 1000); | ||
if (opts.rawBody) { | ||
curl.enable(CurlFeature.Raw); | ||
} | ||
if (opts.noStorage) { | ||
curl.enable(CurlFeature.NoStorage); | ||
} | ||
curl.setOpt('URL', url.href); | ||
@@ -32,3 +90,3 @@ curl.setOpt(Curl.option.VERBOSE, opts.debug); | ||
curl.setOpt(Curl.option.NOPROGRESS, true); | ||
curl.setOpt(Curl.option.TIMEOUT_MS, opts.timeout * 2); | ||
curl.setOpt(Curl.option.TIMEOUT_MS, opts.timeout); | ||
curl.setOpt(Curl.option.MAXREDIRS, opts.maxRedirects); | ||
@@ -38,8 +96,12 @@ curl.setOpt(Curl.option.CUSTOMREQUEST, opts.method); | ||
curl.setOpt(Curl.option.SSL_VERIFYPEER, opts.rejectUnauthorized ? 1 : 0); | ||
curl.setOpt(Curl.option.SSL_VERIFYHOST, 0); | ||
curl.setOpt(Curl.option.PROXY_SSL_VERIFYPEER, opts.rejectUnauthorizedProxy ? 1 : 0); | ||
curl.setOpt(Curl.option.SSL_VERIFYHOST, opts.rejectUnauthorized ? 2 : 0); | ||
curl.setOpt(Curl.option.PROXY_SSL_VERIFYHOST, opts.rejectUnauthorizedProxy ? 2 : 0); | ||
curl.setOpt(Curl.option.CONNECTTIMEOUT_MS, opts.timeout); | ||
if (opts.keepAlive === true) { | ||
curl.setOpt(Curl.option.TCP_KEEPALIVE, 1); | ||
} else { | ||
curl.setOpt(Curl.option.TCP_KEEPALIVE, 0); | ||
} | ||
curl.setOpt(Curl.option.ACCEPT_ENCODING, ''); | ||
@@ -51,4 +113,13 @@ const customHeaders = []; | ||
let lcHeader = ''; | ||
for (let header in opts.headers) { | ||
if (opts.headers[header]) { | ||
lcHeader = header.toLowerCase(); | ||
if (lcHeader === 'content-type') { | ||
hasContentType = true; | ||
} else if (lcHeader === 'content-length') { | ||
hasContentLength = true; | ||
} else if (lcHeader === 'accept-encoding') { | ||
hasAcceptEncoding = opts.headers[header]; | ||
} | ||
customHeaders.push(`${header}: ${opts.headers[header]}`); | ||
@@ -58,2 +129,8 @@ } | ||
if (!hasAcceptEncoding) { | ||
curl.setOpt(Curl.option.ACCEPT_ENCODING, ''); | ||
} else { | ||
curl.setOpt(Curl.option.ACCEPT_ENCODING, hasAcceptEncoding); | ||
} | ||
if (opts.auth) { | ||
@@ -63,116 +140,121 @@ customHeaders.push(`Authorization: Basic ${Buffer.from(opts.auth).toString('base64')}`); | ||
opts.debug && console.info('[request-libcurl] REQUEST:', url.href); | ||
if (libcurl.onData) { | ||
curl.on('data', libcurl.onData); | ||
} | ||
const stopRetryTimeout = () => { | ||
if (retryTimer) { | ||
clearTimeout(retryTimer); | ||
retryTimer = null; | ||
} | ||
}; | ||
if (libcurl.onHeader) { | ||
curl.on('header', libcurl.onHeader); | ||
} | ||
const stopRequestTimeout = () => { | ||
if (timeoutTimer) { | ||
clearTimeout(timeoutTimer); | ||
timeoutTimer = null; | ||
} | ||
stopRetryTimeout(); | ||
}; | ||
curl.on('end', (statusCode, body, _headers) => { | ||
libcurl._debug('[END EVENT]', opts.retries, url.href, finished, statusCode); | ||
stopRequestTimeout(); | ||
libcurl.stopRequestTimeout(); | ||
if (finished) { return; } | ||
const retry = () => { | ||
opts.debug && console.info('[request-libcurl] REQUEST RETRY:', opts.retries, opts.retryDelay, url.href); | ||
stopRetryTimeout(); | ||
retryTimer = setTimeout(() => { | ||
--opts.retries; | ||
request(opts, cb); | ||
}, opts.retryDelay); | ||
}; | ||
finished = true; | ||
curl.close(); | ||
const promise = new Promise((resolve, _reject) => { | ||
reject = _reject; | ||
curl.on('end', (statusCode, body, _headers) => { | ||
opts.debug && console.info('[request-libcurl] REQUEST END:', opts.retries, url.href, statusCode); | ||
stopRequestTimeout(); | ||
if (finished) { return; } | ||
curl.close(); | ||
if ((opts.isBadStatus(statusCode || 408, opts.badStatuses)) && opts.retry === true && opts.retries > 1) { | ||
retry(); | ||
} else { | ||
finished = true; | ||
const headers = {}; | ||
if (_headers && _headers[0]) { | ||
delete _headers[0].result; | ||
for (let headerName in _headers[0]) { | ||
if (_headers[0][headerName]) { | ||
headers[headerName.toLowerCase()] = _headers[0][headerName]; | ||
} | ||
} | ||
const headers = {}; | ||
if (_headers && _headers[0]) { | ||
delete _headers[0].result; | ||
for (let headerName in _headers[0]) { | ||
if (_headers[0][headerName]) { | ||
headers[headerName.toLowerCase()] = _headers[0][headerName]; | ||
} | ||
cb ? cb(void 0, {statusCode, body, headers}) : resolve({statusCode, body, headers}); | ||
} | ||
}); | ||
} | ||
curl.on('error', (error, errorCode) => { | ||
opts.debug && console.error('[request-libcurl] REQUEST ERROR:', opts.retries, url.href, {error, errorCode}); | ||
stopRequestTimeout(); | ||
if (finished) { return; } | ||
curl.close(); | ||
cb(void 0, {statusCode, status: statusCode, body, headers}); | ||
}); | ||
error.code = errorCode; | ||
error.message = typeof error.toString === 'function' ? error.toString() : 'Error occurred during request'; | ||
error.errorCode = errorCode; | ||
error.statusCode = 408; | ||
curl.on('error', (error, errorCode) => { | ||
libcurl._debug('REQUEST ERROR:', opts.retries, url.href, {error, errorCode}); | ||
stopRequestTimeout(); | ||
libcurl.stopRequestTimeout(); | ||
if (finished) { return; } | ||
if (opts.retry === true && opts.retries > 1) { | ||
retry(); | ||
} else { | ||
finished = true; | ||
curl.close(); | ||
let statusCode = 408; | ||
if (errorCode === 52) { | ||
statusCode = 503; | ||
} else if (errorCode === 47) { | ||
statusCode = 429; | ||
} else if (errorCode === 60 || errorCode === 91) { | ||
statusCode = 526; | ||
} | ||
error.code = errorCode; | ||
error.status = statusCode; | ||
error.message = typeof error.toString === 'function' ? error.toString() : 'Error occurred during request'; | ||
error.errorCode = errorCode; | ||
error.statusCode = statusCode; | ||
cb(error); | ||
}); | ||
if (opts.form) { | ||
if (typeof opts.form === 'object') { | ||
isJsonUpload = true; | ||
} | ||
if (typeof opts.form !== 'string') { | ||
try { | ||
opts.form = JSON.stringify(opts.form); | ||
} catch (e) { | ||
libcurl._debug('Can\'t stringify opts.form in POST request :', url.href, e); | ||
finished = true; | ||
cb ? cb(error) : reject(error); | ||
process.nextTick(() => { | ||
libcurl.finished = true; | ||
curl.close(); | ||
cb(badRequestError); | ||
}); | ||
return curl; | ||
} | ||
}); | ||
} | ||
if (opts.method === 'POST' && opts.form) { | ||
if (typeof opts.form !== 'string') { | ||
try { | ||
opts.form = JSON.stringify(opts.form); | ||
} catch (e) { | ||
opts.debug && console.error('[request-libcurl] Can\'t stringify opts.form in POST request :', url.href, e); | ||
const error = { | ||
code: 43, | ||
message: '408: Bad request', | ||
errorCode: 43, | ||
statusCode: 400 | ||
}; | ||
cb ? cb(error) : reject(error); | ||
} | ||
if (!hasContentType) { | ||
if (isJsonUpload) { | ||
customHeaders.push('Content-Type: application/json'); | ||
} else { | ||
customHeaders.push('Content-Type: application/x-www-form-urlencoded'); | ||
} | ||
} | ||
customHeaders.push('Content-Type: application/x-www-form-urlencoded'); | ||
if (!hasContentLength) { | ||
customHeaders.push(`Content-Length: ${Buffer.byteLength(opts.form)}`); | ||
curl.setOpt(Curl.option.POSTFIELDS, opts.form); | ||
} | ||
curl.setOpt(Curl.option.HTTPHEADER, customHeaders); | ||
curl.setOpt(Curl.option.POSTFIELDS, opts.form); | ||
} else if (opts.upload) { | ||
curl.setOpt(Curl.option.UPLOAD, true); | ||
curl.setOpt(Curl.option.READDATA, opts.upload); | ||
} | ||
timeoutTimer = setTimeout(() => { | ||
stopRequestTimeout(); | ||
if (finished) { return; } | ||
curl.close(); | ||
if (opts.retries > 0) { | ||
retry(); | ||
} else { | ||
finished = true; | ||
const error = { | ||
code: 28, | ||
message: '408: host unreachable, request timeout', | ||
errorCode: 28, | ||
statusCode: 408 | ||
}; | ||
cb ? cb(error) : reject(error); | ||
if (opts.curlOptions && typeof opts.curlOptions === 'object') { | ||
for (let option in opts.curlOptions) { | ||
if (Curl.option[option] !== undefined) { | ||
curl.setOpt(Curl.option[option], opts.curlOptions[option]); | ||
} | ||
}, opts.timeout + 2500); | ||
} | ||
} | ||
if (!opts.wait) { | ||
if (sent) { return; } | ||
sent = true; | ||
if (opts.curlFeatures && typeof opts.curlFeatures === 'object') { | ||
for (let option in opts.curlFeatures) { | ||
if (CurlFeature[option] !== undefined) { | ||
if (opts.curlFeatures[option] === true) { | ||
curl.enable(CurlFeature[option]); | ||
} else if (opts.curlFeatures[option] === false) { | ||
curl.disable(CurlFeature[option]); | ||
} | ||
} | ||
} | ||
} | ||
curl.setOpt(Curl.option.HTTPHEADER, customHeaders); | ||
process.nextTick(() => { | ||
if (!libcurl.finished) { | ||
curl.perform(); | ||
@@ -182,23 +264,142 @@ } | ||
promise.request = curl; | ||
promise.send = () => { | ||
if (sent) { return; } | ||
sent = true; | ||
curl.perform(); | ||
}; | ||
promise.abort = () => { | ||
if (finished) { return; } | ||
finished = true; | ||
curl.close(); | ||
const error = { | ||
code: 42, | ||
message: '499: Client Closed Request', | ||
errorCode: 42, | ||
statusCode: 499 | ||
}; | ||
cb ? cb(error) : reject(error); | ||
}; | ||
return promise; | ||
return curl; | ||
}; | ||
class LibCurlRequest { | ||
constructor (opts, cb) { | ||
let isBadUrl = false; | ||
this.cb = cb || noop; | ||
this.sent = false; | ||
this.finished = false; | ||
this.retryTimer = false; | ||
this.timeoutTimer = null; | ||
this.opts = Object.assign({}, request.defaultOptions, opts); | ||
this.opts.method = this.opts.method.toUpperCase(); | ||
if (this.opts.debug) { | ||
this._debug = _debug; | ||
} else { | ||
this._debug = noop; | ||
} | ||
this._debug('[constructor]', this.opts.uri || this.opts.url); | ||
this.stopRequestTimeout = () => { | ||
if (this.timeoutTimer) { | ||
clearTimeout(this.timeoutTimer); | ||
this.timeoutTimer = null; | ||
} | ||
}; | ||
if (!this.opts.uri && !this.opts.url) { | ||
this._debug('REQUEST: NO URL PROVIDED ERROR:', opts); | ||
isBadUrl = true; | ||
} else { | ||
try { | ||
this.url = new URL(this.opts.uri || this.opts.url); | ||
} catch (urlError) { | ||
this._debug('REQUEST: `new URL()` ERROR:', opts, urlError); | ||
isBadUrl = true; | ||
} | ||
} | ||
if (isBadUrl) { | ||
this.sent = true; | ||
this.finished = true; | ||
process.nextTick(() => { | ||
this.cb(badUrlError); | ||
}); | ||
return; | ||
} | ||
if (!this.opts.wait) { | ||
this.send(); | ||
} | ||
} | ||
onData(callback) { | ||
this.onData = callback; | ||
} | ||
onHeader(callback) { | ||
this.onHeader = callback; | ||
} | ||
_retry() { | ||
this._debug('[_retry]', this.opts.retry, this.opts.retries, this.opts.uri || this.opts.url); | ||
if (this.opts.retry === true && this.opts.retries > 0) { | ||
--this.opts.retries; | ||
this.retryTimer = setTimeout(() => { | ||
this.currentCurl = sendRequest(this, this.url, this._sendRequestCallback.bind(this)); | ||
}, this.opts.retryDelay); | ||
return true; | ||
} | ||
return false; | ||
} | ||
_sendRequestCallback(error, result) { | ||
this._debug('[_sendRequestCallback]', this.opts.uri || this.opts.url); | ||
let isRetry = false; | ||
let statusCode = 408; | ||
if (result && result.statusCode) { | ||
statusCode = result.statusCode; | ||
} else if (error && error.statusCode) { | ||
statusCode = error.statusCode; | ||
} | ||
if (error && error.errorCode !== 47) { | ||
isRetry = this._retry(); | ||
} else if ((this.opts.isBadStatus(statusCode, this.opts.badStatuses)) && this.opts.retry === true && this.opts.retries > 1) { | ||
isRetry = this._retry(); | ||
} | ||
if (!isRetry) { | ||
this.finished = true; | ||
if (error) { | ||
this.cb(error); | ||
} else { | ||
this.cb(void 0, result); | ||
} | ||
} | ||
} | ||
send() { | ||
this._debug('[send]', this.opts.uri || this.opts.url); | ||
if (this.sent) { | ||
return this; | ||
} | ||
this.sent = true; | ||
this.timeoutTimer = setTimeout(() => { | ||
this.abort(); | ||
}, ((this.opts.timeout + this.opts.retryDelay) * (this.opts.retries + 1))); | ||
this.curl = sendRequest(this, this.url, this._sendRequestCallback.bind(this)); | ||
return this; | ||
} | ||
abort() { | ||
this._debug('[abort]', this.opts.uri || this.opts.url); | ||
if (this.retryTimer) { | ||
clearTimeout(this.retryTimer); | ||
this.retryTimer = false; | ||
} | ||
if (!this.finished) { | ||
closeCurl(this.curl); | ||
this.finished = true; | ||
this.cb(abortError); | ||
} | ||
return this; | ||
} | ||
} | ||
function request (opts, cb) { | ||
return new LibCurlRequest(opts, cb); | ||
} | ||
request.defaultOptions = { | ||
@@ -219,3 +420,4 @@ wait: false, | ||
rejectUnauthorized: false, | ||
badStatuses: [300, 303, 305, 400, 407, 408, 409, 410, 500, 510], | ||
rejectUnauthorizedProxy: false, | ||
badStatuses: [300, 303, 305, 400, 407, 408, 409, 410, 500, 502, 503, 504, 510], | ||
isBadStatus(statusCode, badStatuses = request.defaultOptions.badStatuses) { | ||
@@ -222,0 +424,0 @@ return badStatuses.includes(statusCode) || statusCode >= 500; |
{ | ||
"name": "request-libcurl", | ||
"version": "1.0.3", | ||
"description": "Extremely stable HTTP request module built on top of libcurl with retries, timeouts, Promise and async/await API", | ||
"version": "2.0.0", | ||
"description": "Extremely stable HTTP request module built on top of libcurl", | ||
"main": "index.js", | ||
"scripts": {}, | ||
"scripts": { | ||
"test": "mocha ./test/npm.js" | ||
}, | ||
"repository": { | ||
@@ -34,6 +36,9 @@ "type": "git", | ||
"homepage": "https://github.com/VeliovGroup/request-extra#readme", | ||
"devDependencies": {}, | ||
"devDependencies": { | ||
"chai": "^4.2.0", | ||
"mocha": "^6.2.1" | ||
}, | ||
"dependencies": { | ||
"node-libcurl": "^2.0.1" | ||
"node-libcurl": "^2.0.2" | ||
} | ||
} |
342
README.md
@@ -1,2 +0,2 @@ | ||
# Request-Extra | ||
# Request-libcurl | ||
@@ -13,14 +13,37 @@ <a href="https://www.patreon.com/bePatron?u=20396046"> | ||
## Main features: | ||
## Main features | ||
- 👷♂️ Follow `request` API; | ||
- 👨💻 98% tests coverage + TDD (*only for http(s)*); | ||
- 👷♂️ Follow `request` API (*simplified*); | ||
- 📦 The single dependency on `node-libcurl` package; | ||
- 😎 IDNs support (*internationalized domain names*); | ||
- 😎 Repeat (*built-in retries*) request on broken connection; | ||
- 😎 HTTP/2 support; | ||
- 😎 Repeat (*built-in retries*) request on failed or broken connection; | ||
- 😎 Send GET/POST with custom `body` and headers; | ||
- 😎 Follow or deny redirects; | ||
- 💪 Bulletproof design, during development we plan to avoid complex solutions. | ||
- 📤 Upload files with a single line; | ||
- 🔐 Ignore or deny "broken" SSL/TLS certificates; | ||
- 💪 Bulletproof design, during development we avoid complex solutions. | ||
## Install: | ||
## ToC: | ||
- [Installation](https://github.com/VeliovGroup/request-extra#install) | ||
- [API](https://github.com/VeliovGroup/request-extra#api) | ||
- [Request options *detailed* description](https://github.com/VeliovGroup/request-extra#request-options): | ||
- [response description (*success*)](https://github.com/VeliovGroup/request-extra#response) | ||
- [error description (*fail*)](https://github.com/VeliovGroup/request-extra#error) | ||
- [*LibCurlRequest* API](https://github.com/VeliovGroup/request-extra#returns-req-object) | ||
- [List of default request options](https://github.com/VeliovGroup/request-extra#request-default-options) | ||
- [Examples](https://github.com/VeliovGroup/request-extra#examples): | ||
- [GET](https://github.com/VeliovGroup/request-extra#get-request) | ||
- [POST](https://github.com/VeliovGroup/request-extra#post-request) | ||
- [POST (*advanced*)](https://github.com/VeliovGroup/request-extra#post-request-with-extra-options) | ||
- [File upload](https://github.com/VeliovGroup/request-extra#file-upload) | ||
- [File upload (*multipart*)](https://github.com/VeliovGroup/request-extra#file-upload-multipartform-data) | ||
- [Running tests](https://github.com/VeliovGroup/request-extra#running-tests) | ||
- [Tests](https://github.com/VeliovGroup/request-extra/blob/master/test/npm.js) | ||
- [Support](https://github.com/VeliovGroup/request-extra#support-our-open-source-contribution) | ||
## Install | ||
```shell | ||
@@ -39,7 +62,7 @@ # ONLY for node@>=8.9.0 | ||
## Note: | ||
## Note | ||
We build this package to serve our needs and solve our issues with Node's native API. It may have a lack of compatibility with `request()` module API, or compatible only partially. Use on your own risk or wait for stable `v1.0.0`. | ||
We build this package to serve our needs and solve our issues with Node's native API. It may have a lack of compatibility with `request()` module API, or compatible only partially. | ||
## API: | ||
## API | ||
@@ -51,3 +74,3 @@ ```js | ||
method: 'GET', // POST, GET | ||
uri: 'https://example.com', // String | ||
url: 'https://example.com', // String | ||
auth: 'username:password', // String | ||
@@ -70,23 +93,9 @@ form: '{"ops": "value"}', // String, can be JSON or any other type of payload | ||
// Promise API v1 | ||
const promise = request(opts); | ||
// Promise API v2 | ||
request(opts).then((resp) => { | ||
const { statusCode, body, headers } = resp; | ||
}).catch((error) => { | ||
const { errorCode, code, statusCode, message } = error; | ||
}); | ||
// Async/Await | ||
async function get (opts) { | ||
const { statusCode, body, headers } = await request(opts); | ||
return body; | ||
} | ||
// Callback API | ||
request(opts, (error, resp) => { | ||
if (error) { | ||
// Houston we got a problem! 😱 | ||
const { errorCode, code, statusCode, message } = error; | ||
} else { | ||
// We've got successful response! 🥳 | ||
const { statusCode, body, headers } = resp; | ||
@@ -97,9 +106,56 @@ } | ||
### Request options: | ||
### Request default options | ||
- `opts.uri` {*String*} - [__Required__] Fully qualified URI with protocol `http`/`https`; | ||
- `opts.method` {*String*} - [Optional] HTTP Method name, you can use any valid method name from HTTP specs, tested with GET/POST, default: `GET`. If set to POST, `Content-Type: application/x-www-form-urlencoded` HTTP header would be set; | ||
- `opts.auth` {*String*} - [Optional] value for HTTP Authorization header as plain string; | ||
- `opts.form` {*String*|*Object*} - [Optional] Custom request body for POST request; | ||
- `opts.headers` {*Object*} - [Optional] Custom request headers, default: `{ Accept: '*/*', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36' }`; | ||
```js | ||
const request = require('request-libcurl'); | ||
// Default "defaultOptions" Object: | ||
request.defaultOptions = { | ||
wait: false, | ||
proxy: false, | ||
retry: true, | ||
debug: false, | ||
method: 'GET', | ||
timeout: 6144, | ||
retries: 3, | ||
rawBody: false, | ||
keepAlive: false, | ||
noStorage: false, | ||
retryDelay: 256, | ||
maxRedirects: 4, | ||
followRedirect: true, | ||
rejectUnauthorized: false, | ||
badStatuses: [ 300, 303, 305, 400, 407, 408, 409, 410, 500, 502, 503, 504, 510 ], | ||
isBadStatus(statusCode, badStatuses = request.defaultOptions.badStatuses) { | ||
return badStatuses.includes(statusCode) || statusCode >= 500; | ||
}, | ||
headers: { | ||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36', | ||
Accept: '*/*' | ||
} | ||
}; | ||
// Override default settings: | ||
request.defaultOptions.timeout = 7000; | ||
request.defaultOptions.retries = 12; | ||
request.defaultOptions.retryDelay = 5000; | ||
request.defaultOptions.followRedirect = false; | ||
// Override bad statuses codes (used to trigger retries) | ||
request.defaultOptions.badStatuses = [300, 303, 305, 400, 407, 408, 409, 410]; | ||
// Override function used to trigger retries based on status code | ||
request.defaultOptions.isBadStatus = (statusCode, badStatuses = request.defaultOptions.badStatuses) => { | ||
return badStatuses.includes(statusCode) || statusCode >= 500; | ||
}; | ||
``` | ||
### Request options | ||
- `opts.url` or `opts.uri` {*String*} - [__Required__] Fully qualified URI with protocol `http`/`https`; | ||
- `opts.method` {*String*} - [Optional] HTTP Method name, you can use any valid method name from HTTP specs, tested with GET/POST, default: `GET`; | ||
- `opts.auth` {*String*} - [Optional] value for HTTP Authorization header as plain string in a form of `username:password`; | ||
- `opts.form` {*String*|*Object*} - [Optional] Custom request body for POST request. If {*String*} is passed `Content-Type` will be set to `application/x-www-form-urlencoded`, by passing plain {*Object*} `Content-Type` will be set to `application/json`. To set custom `Content-Type` — pass it to `opts.headers` *Object*; | ||
- `opts.upload` {*Integer*} - [Optional] To upload a file pass an *Integer* representing the *file descriptor*. See [this example](https://github.com/VeliovGroup/request-extra#file-upload) for reference; | ||
- `opts.headers` {*Object*} - [Optional] Custom request headers, default: `{ Accept: '*/*', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36' }`. Note: setting custom request headers will replace default ones; | ||
- `opts.debug` {*Boolean*} - [Optional] Enable debug and extra logging, default: `false`; | ||
@@ -113,20 +169,35 @@ - `opts.retry` {*Boolean*} - [Optional] Retry request if connection is broken? Default: `true`; | ||
- `opts.maxRedirects` {*Number*} - [Optional] How many redirects are supported during single request, default: `4`; | ||
- `opts.badStatuses` {*[Number]*} - [Optional] Array of "bad" status codes responsible for triggering request retries, default: `[300, 303, 305, 400, 407, 408, 409, 410, 500, 510]`; | ||
- `opts.isBadStatus` {*Function*} - [Optional] Function responsible for triggering request retries, default: *see at the bottom of examples section*; | ||
- `opts.rawBody` and `opts.noStorage` {*Boolean*} - Disable all data processing, great option for *piping*, default: `false`; | ||
- `opts.wait` {*Boolean*} - Do not send request immediately and wait until `.send()` method is called, set this option to `true` to listen events on `promise.request` object, default: `false`; | ||
- `opts.badStatuses` {*[Number]*} - [Optional] Array of "bad" status codes responsible for triggering request retries, default: `[300, 303, 305, 400, 407, 408, 409, 410, 500, 502, 503, 504, 510]`; | ||
- `opts.isBadStatus` {*Function*} - [Optional] Function responsible for triggering request retries, [default (*at the bottom of code-block*)](https://github.com/VeliovGroup/request-extra#request-default-options); | ||
- `opts.rawBody` {*Boolean*} - Disable all data processing (`body` will be passed as *Buffer*, `headers` will be empty, use `.onHeaders()` hook to get headers with `rawBody` option), great option for *piping*, default: `false`; | ||
- `opts.noStorage` {*Boolean*} - Disable all data processing and data concatenation (`headers` and `body` won't be passed to response), great option for *piping*, default: `false`; | ||
- `opts.wait` {*Boolean*} - Do not send request immediately and wait until `.send()` method is called, set this option to `true` to register `.onHeaders()` and `.onBody()` hooks, default: `false`; | ||
- `opts.proxy` {*String*} - Fully qualified URL to HTTP proxy, when this feature is enabled connections are going to start with `CONNECT` request, default: no proxy or system proxy is used; | ||
- `opts.rejectUnauthorized` {*Boolean*} - [Optional] Shall request continue if SSL/TLS certificate can't be validated? Default: `false`. | ||
- `opts.rejectUnauthorized` {*Boolean*} - [Optional] Shall request be rejected if SSL/TLS certificate can't be validated? Default: `false`; | ||
- `opts.rejectUnauthorizedProxy` {*Boolean*} - [Optional] Shall request be rejected if SSL/TLS certificate of a __proxy host__ can't be validated? Default: `false`; | ||
- `opts.curlOptions` {*Object*} - [Optional] Explicitly set `libcurl` options, full list of options available [here](https://curl.haxx.se/libcurl/c/curl_easy_setopt.html) and [here](https://github.com/JCMais/node-libcurl/blob/32647acc28b026bbd03aa1e3abea9cbbc8f7d43b/lib/generated/CurlOption.ts#L3286); | ||
- `opts.curlFeatures` {*Object*} - [Optional] Explicitly __enable__ or __disable__ `libcurl` features. To __enable__ a feature pass `true` as a value, example: `{NoDataParsing: true}`. To __disable__ pass `false` as a value, example: `{NoDataParsing: false}`. Full list of available features is available [here](https://github.com/JCMais/node-libcurl/blob/249323f0f3883f8cbf6d0e91a89bfecb5862da53/lib/enum/CurlFeature.ts#L7). | ||
__Note__: When using `opts.rawBody` or `opts.noStorage` callback/promise won't return `body` and `headers`, to get headers and body use next events: | ||
__Notes__: | ||
- When using `opts.rawBody` callback won't return `headers`, to get headers use `onHeaders` hook; | ||
- When using `opts.noStorage` callback won't return `headers` and `body`, to get headers and body use `onData` and `onHeaders` hooks; | ||
- `opts.upload` and `opts.form` __can not be used together__, there won't be exception thrown, if both presented — `opts.form` will be used; | ||
- When using `opts.upload` or __any other request where server returns__ `expect: '100-continue'` HTTP header — callback won't return `headers`, to get headers use `onHeaders` hook; | ||
- This package is build on top of [`libcurl`](https://curl.haxx.se/libcurl/) and [`node-libcurl`](https://github.com/JCMais/node-libcurl) it's the way much more powerful than just sending requests via `http` and `https` protocol. Libcurl can work with IMAP/SMTP protocols getting/sending emails. Libcurl can serve as fully-featured FTP-client. Here's full list of supported protocols: `DICT`, `FILE`, `FTP`, `FTPS`, `Gopher`, `HTTP`, `HTTPS`, `IMAP`, `IMAPS`, `LDAP`, `LDAPS`, `POP3`, `POP3S`, `RTMP`, `RTSP`, `SCP`, `SFTP`, `SMTP`, `SMTPS`, `Telnet` and `TFTP`. To learn more on how to utilize all available power and features see docs of [`node-libcurl`](https://github.com/JCMais/node-libcurl#node-libcurl) and [`libcurl`](https://curl.haxx.se/libcurl/) itself. | ||
```js | ||
let _body = Buffer.from(''); | ||
let _headers = Buffer.from(''); | ||
let _body = Buffer.from(''); | ||
let _headers = Buffer.from(''); | ||
const headersObj = {}; | ||
const promise = request({ | ||
uri: 'https://example.com', | ||
const req = request({ | ||
url: 'https://example.com', | ||
retry: false, // Do not retry with rawBody/noStorage, as it may mess up with headers and body inside `.onData()` and `.onHeader()` hooks | ||
rawBody: true, | ||
wait: true | ||
}).then((status) => { | ||
wait: true // Using 'wait' option to set `.onData()` and `.onHeader()` hooks | ||
}, (error) => { | ||
if (error) { | ||
throw error; | ||
} | ||
const body = _body.toString('utf8'); | ||
@@ -136,14 +207,23 @@ const headers = _headers.toString('utf8'); | ||
promise.request.on('data', (chunkAsBuffer) => { | ||
req.onData((chunkAsBuffer) => { | ||
// Do something with a body | ||
// .pipe() for example | ||
_body = Buffer.concat([_body, chunkAsBuffer]); | ||
}); | ||
promise.request.on('header', (chunkAsBuffer) => { | ||
req.onHeader((chunkAsBuffer) => { | ||
_headers = Buffer.concat([_headers, chunkAsBuffer]); | ||
// or convert it to headers Object: | ||
const header = chunkAsBuffer.toString('utf8'); | ||
if (header.includes(':')) { | ||
const splitHeader = header.split(':'); | ||
headersObj[splitHeader[0].toLowerCase().trim()] = splitHeader[1].trim(); | ||
} | ||
}); | ||
promise.send(); | ||
req.send(); | ||
``` | ||
### Response: | ||
### Response | ||
@@ -154,3 +234,3 @@ - `resp.statusCode` {*Number*} - HTTP response/status code; | ||
### Error: | ||
### Error | ||
@@ -162,17 +242,19 @@ - `error.errorCode` {*Number*} - `libcurl` internal error code; | ||
### Returns extended {*Promise*} Object: | ||
### Returns `req` *Object* | ||
```js | ||
const request = require('request-libcurl'); | ||
const promise = request({uri: 'https://example.com'}); | ||
const req = request({url: 'https://example.com'}); | ||
```` | ||
- `promise.abort()` - Abort current request, request will return `499: Client Closed Request` HTTP error | ||
- `promise.send()` - Send request, useful with `wait` and `rawBody`, when you need to delay sending request, for example to set event listeners | ||
- `promise.request` {*ClientRequest*} - See [`node-libcurl` docs](https://github.com/JCMais/node-libcurl) | ||
- `promise.then(resp)` - Callback triggered on successful response | ||
- `resp.statusCode` {*Number*} - HTTP status code | ||
- `req.abort()` - Abort current request, request will return `499: Client Closed Request` HTTP error | ||
- `req.send()` - Send request, use it with `wait`. For example with `rawBody`/`noStorage`, when you need to delay sending request, for example to set event listeners and/or hooks | ||
- `req.onData(callback)` - Hook, called right after data is received, called for each data-chunk. Useful with `.pipe()`, `rawBody`/`noStorage` and hooks/events | ||
- `req.onHeader(callback)` - Hook, called right after header is received, called for each header. Useful with `.pipe()`, `rawBody`/`noStorage` and hooks/events | ||
- `callback(error, resp)` - Callback triggered on successful response | ||
- `error` {*undefined*}; | ||
- `resp.statusCode` {*Number*} - HTTP status code; | ||
- `resp.body` {*String*} - Body of HTTP response, not modified or processed, as it is — plain text; | ||
- `resp.headers` {*Object*} - Key-value plain *Object* with pairs of response headers | ||
- `promise.catch(error)` - Callback triggered on failed request | ||
- `resp.headers` {*Object*} - Key-value plain *Object* with pairs of response headers; | ||
- `callback(error)` - Callback triggered on failed request | ||
- `error.errorCode` {*Number*} - `libcurl` internal error code; | ||
@@ -183,17 +265,27 @@ - `error.code` {*Number*} - `libcurl` internal error code, same as `errorCode`; | ||
## Examples: | ||
## Examples | ||
### GET request | ||
```js | ||
const request = require('request-libcurl'); | ||
// Simple GET: | ||
request({ uri: 'https://example.com' }, (error, resp) => { | ||
// GET request: | ||
request({ url: 'https://example.com' }, (error, resp) => { | ||
/* ... */ | ||
}); | ||
``` | ||
// Simple POST: | ||
### POST request | ||
```js | ||
const request = require('request-libcurl'); | ||
const querystring = require('querystring'); | ||
// POST (Content-Type: application/x-www-form-urlencoded): | ||
// by passing a String or formatted "Query String" to `form` | ||
request({ | ||
method: 'POST', | ||
uri: 'https://example.com', | ||
form: JSON.stringify({ myForm: 'data' }) | ||
url: 'https://example.com', | ||
form: querystring.stringify({ myForm: 'data' }) | ||
}, (error, resp) => { | ||
@@ -203,8 +295,9 @@ /* ... */ | ||
// POST request with Authorization: | ||
// POST with Authorization (Content-Type: application/x-www-form-urlencoded): | ||
// by passing a String or formatted "Query String" to `form` | ||
request({ | ||
method: 'POST', | ||
uri: 'https://example.com', | ||
url: 'https://example.com', | ||
auth: 'username:passwd', | ||
form: JSON.stringify({ myForm: 'data' }) | ||
form: querystring.stringify({ myForm: 'data' }) | ||
}, (error, resp) => { | ||
@@ -214,17 +307,97 @@ /* ... */ | ||
// Override default settings: | ||
request.defaultOptions.timeout = 7000; | ||
request.defaultOptions.retries = 12; | ||
request.defaultOptions.retryDelay = 5000; | ||
request.defaultOptions.followRedirect = false; | ||
// POST (Content-Type: application/json): | ||
// by passing plain Object to `form` | ||
request({ | ||
method: 'POST', | ||
url: 'https://example.com', | ||
form: { myForm: 'data' } | ||
}, (error, resp) => { | ||
/* ... */ | ||
}); | ||
``` | ||
// Override bad statuses codes (used to trigger retries) | ||
request.defaultOptions.badStatuses = [300, 303, 305, 400, 407, 408, 409, 410]; | ||
### POST request with extra options | ||
// Override function used to trigger retries based on status code | ||
request.defaultOptions.isBadStatus = (statusCode, badStatuses = request.defaultOptions.badStatuses) => { | ||
return badStatuses.includes(statusCode) || statusCode >= 500; | ||
}; | ||
```js | ||
const request = require('request-libcurl'); | ||
// POST with Authorization (Content-Type: application/json): | ||
// by passing plain Object to `form` | ||
request({ | ||
method: 'POST', | ||
url: 'https://example.com', | ||
auth: 'username:passwd', | ||
form: { myForm: 'data' } | ||
}, (error, resp) => { | ||
/* ... */ | ||
}); | ||
// Custom POST (Content-Type: text/plain): | ||
// by passing custom Headers | ||
request({ | ||
method: 'POST', | ||
url: 'https://example.com', | ||
form: 'Plain String or Base64 String or any other String', | ||
headers: { | ||
'Content-Type': 'text/plain' | ||
} | ||
}, (error, resp) => { | ||
/* ... */ | ||
}); | ||
``` | ||
### File upload | ||
```js | ||
const fs = require('fs'); | ||
const request = require('request-libcurl'); | ||
fs.open('/path/to/a/file', 'r', function(err, fd) { | ||
if (err) { | ||
throw new Error('can not read the file'); | ||
} | ||
request({ | ||
method: 'POST', | ||
url: 'https://example.com/upload', | ||
upload: fd, | ||
retry: false, | ||
}, (error, resp) => { | ||
if (error) { | ||
throw error; | ||
} else { | ||
// File successfully uploaded | ||
} | ||
}); | ||
}); | ||
``` | ||
### File upload (`multipart/form-data`) | ||
In this example we are going to use [`HTTPPOST` libcurl option](https://curl.haxx.se/libcurl/c/CURLOPT_HTTPPOST.html) passing `[Object]` (*array of Objects* representing files, note: multiple files can be passed in a single request) via `curlOptions` | ||
```js | ||
const request = require('request-libcurl'); | ||
const fileLocation = '/full/absolute/path/to/a/file.ext'; | ||
request({ | ||
method: 'POST', // Can be used with PUT | ||
url: 'https://example.com/upload.php', | ||
retry: false, | ||
curlOptions: { | ||
HTTPPOST: [{ | ||
name: 'file.ext', // File's name | ||
file: fileLocation, // Full absolute path to a file on FS | ||
type: 'application/ext' // File's mime-type | ||
} /*, {...} */] | ||
} | ||
}, (error) => { | ||
if (error) { | ||
throw error; | ||
} else { | ||
// File(s) successfully uploaded | ||
} | ||
}); | ||
``` | ||
## Running Tests | ||
@@ -237,8 +410,17 @@ | ||
```shell | ||
# TBD | ||
# Install development NPM dependencies: | ||
npm install --save-dev | ||
# Install NPM dependencies: | ||
npm install --save | ||
# Run tests: | ||
PORT=3003 npm test | ||
# PORT env.var is required! And can be changed to any open port! | ||
# Note: The Internet connection is required to perform tests | ||
# Note: Test-suite includes "no response" and "timeouted responces" | ||
# if a test looks stuck — give it another minute before interrupting it | ||
``` | ||
## Support our open source contribution: | ||
## Support our open source contribution | ||
- [Become a patron](https://www.patreon.com/bePatron?u=20396046) — support my open source contributions with monthly donation | ||
- Use [ostr.io](https://ostr.io) — [Monitoring](https://snmp-monitoring.com), [Analytics](https://ostr.io/info/web-analytics), [WebSec](https://domain-protection.info), [Web-CRON](https://web-cron.info) and [Pre-rendering](https://prerendering.com) for a website |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
38636
356
414
2
1
Updatednode-libcurl@^2.0.2