Comparing version 9.1.0 to 9.2.0
{ | ||
"name": "got", | ||
"version": "9.1.0", | ||
"version": "9.2.0", | ||
"description": "Simplified HTTP requests", | ||
@@ -38,2 +38,3 @@ "license": "MIT", | ||
"@sindresorhus/is": "^0.11.0", | ||
"@szmarczak/http-timer": "^1.1.0", | ||
"cacheable-request": "^4.0.1", | ||
@@ -62,2 +63,3 @@ "decompress-response": "^3.3.0", | ||
"tempy": "^0.2.1", | ||
"tough-cookie": "^2.4.3", | ||
"xo": "^0.22.0" | ||
@@ -64,0 +66,0 @@ }, |
233
readme.md
@@ -92,8 +92,4 @@ <div align="center"> | ||
Returns a Promise for a `response` object with a `body` property, a `url` property with the request URL or the final URL after redirects, and a `requestUrl` property with the original request URL. | ||
Returns a Promise for a [`response` object](#response) or a [stream](#streams-1) if `options.stream` is set to true. | ||
The response object will typically be a [Node.js HTTP response stream](https://nodejs.org/api/http.html#http_class_http_incomingmessage), however, if returned from the cache it will be a [response-like object](https://github.com/lukechilds/responselike) which behaves in the same way. | ||
The response will also have a `fromCache` property set with a boolean value. | ||
##### url | ||
@@ -167,2 +163,10 @@ | ||
###### cookieJar | ||
Type: [`tough.CookieJar` instance](https://github.com/salesforce/tough-cookie#cookiejar) | ||
Cookie support. You don't have to care about parsing or how to store them. [Example.](#cookies) | ||
**Note:** `options.headers.cookie` will be overridden. | ||
###### encoding | ||
@@ -271,2 +275,9 @@ | ||
###### request | ||
Type: `Function`<br> | ||
Default: `http.request` `https.request` *(depending on the protocol)* | ||
Custom request function. The main purpose of this is to [support HTTP2 using a wrapper](#experimental-http2-support). | ||
###### useElectronNet | ||
@@ -288,2 +299,21 @@ | ||
###### agent | ||
Same as the [`agent` option](https://nodejs.org/api/http.html#http_http_request_url_options_callback) for `http.request`, but with an extra feature: | ||
If you require different agents for different protocols, you can pass a map of agents to the `agent` option. This is necessary because a request to one protocol might redirect to another. In such a scenario, Got will switch over to the right protocol agent for you. | ||
```js | ||
const got = require('got'); | ||
const HttpAgent = require('agentkeepalive'); | ||
const {HttpsAgent} = HttpAgent; | ||
got('sindresorhus.com', { | ||
agent: { | ||
http: new HttpAgent(), | ||
https: new HttpsAgent() | ||
} | ||
}); | ||
``` | ||
###### hooks | ||
@@ -307,2 +337,67 @@ | ||
#### Response | ||
The response object will typically be a [Node.js HTTP response stream](https://nodejs.org/api/http.html#http_class_http_incomingmessage), however, if returned from the cache it will be a [response-like object](https://github.com/lukechilds/responselike) which behaves in the same way. | ||
##### body | ||
Type: `string` `Object` *(depending on `options.json`)* | ||
The result of the request. | ||
##### url | ||
Type: `string` | ||
The request URL or the final URL after redirects. | ||
##### requestUrl | ||
Type: `string` | ||
The original request URL. | ||
##### timings | ||
Type: `Object` | ||
The object contains the following properties: | ||
- `start` - Time when the request started. | ||
- `socket` - Time when a socket was assigned to the request. | ||
- `lookup` - Time when the DNS lookup finished. | ||
- `connect` - Time when the socket successfully connected. | ||
- `upload` - Time when the request finished uploading. | ||
- `response` - Time when the request fired the `response` event. | ||
- `end` - Time when the response fired the `end` event. | ||
- `error` - Time when the request fired the `error` event. | ||
- `phases` | ||
- `wait` - `timings.socket - timings.start` | ||
- `dns` - `timings.lookup - timings.socket` | ||
- `tcp` - `timings.connect - timings.lookup` | ||
- `request` - `timings.upload - timings.connect` | ||
- `firstByte` - `timings.response - timings.upload` | ||
- `download` - `timings.end - timings.response` | ||
- `total` - `timings.end - timings.start` or `timings.error - timings.start` | ||
**Note**: The time is a `number` representing the milliseconds elapsed since the UNIX epoch. | ||
##### fromCache | ||
Type: `boolean` | ||
Whether the response was retrieved from the cache. | ||
##### redirectUrls | ||
Type: `Array` | ||
The redirect URLs. | ||
##### retryCount | ||
Type: `number` | ||
The number of times the request was retried. | ||
#### Streams | ||
@@ -316,3 +411,3 @@ | ||
Returna a [duplex stream](https://nodejs.org/api/stream.html#stream_class_stream_duplex) with additional events: | ||
Returns a [duplex stream](https://nodejs.org/api/stream.html#stream_class_stream_duplex) with additional events: | ||
@@ -578,3 +673,3 @@ ##### .on('request', request) | ||
You can use the [`tunnel`](https://github.com/koichik/node-tunnel) module with the `agent` option to work with proxies: | ||
You can use the [`tunnel`](https://github.com/koichik/node-tunnel) package with the `agent` option to work with proxies: | ||
@@ -594,17 +689,2 @@ ```js | ||
If you require different agents for different protocols, you can pass a map of agents to the `agent` option. This is necessary because a request to one protocol might redirect to another. In such a scenario, `got` will switch over to the right protocol agent for you. | ||
```js | ||
const got = require('got'); | ||
const HttpAgent = require('agentkeepalive'); | ||
const HttpsAgent = HttpAgent.HttpsAgent; | ||
got('sindresorhus.com', { | ||
agent: { | ||
http: new HttpAgent(), | ||
https: new HttpsAgent() | ||
} | ||
}); | ||
``` | ||
Check out [`global-tunnel`](https://github.com/np-maintain/global-tunnel) if you want to configure proxy support for all HTTP/HTTPS traffic in your app. | ||
@@ -615,22 +695,12 @@ | ||
You can use the [`cookie`](https://github.com/jshttp/cookie) module to include cookies in a request: | ||
You can use the [`tough-cookie`](https://github.com/salesforce/tough-cookie) package: | ||
```js | ||
const got = require('got'); | ||
const cookie = require('cookie'); | ||
const {CookieJar} = require('tough-cookie'); | ||
got('google.com', { | ||
headers: { | ||
cookie: cookie.serialize('foo', 'bar') | ||
} | ||
}); | ||
const cookieJar = new CookieJar(); | ||
cookieJar.setCookie('foo=bar', 'https://www.google.com'); | ||
got('google.com', { | ||
headers: { | ||
cookie: [ | ||
cookie.serialize('foo', 'bar'), | ||
cookie.serialize('fizz', 'buzz') | ||
].join(';') | ||
} | ||
}); | ||
got('google.com', {cookieJar}); | ||
``` | ||
@@ -641,3 +711,3 @@ | ||
You can use the [`form-data`](https://github.com/form-data/form-data) module to create POST request with form data: | ||
You can use the [`form-data`](https://github.com/form-data/form-data) package to create POST request with form data: | ||
@@ -660,3 +730,3 @@ ```js | ||
You can use the [`oauth-1.0a`](https://github.com/ddo/oauth-1.0a) module to create a signed OAuth request: | ||
You can use the [`oauth-1.0a`](https://github.com/ddo/oauth-1.0a) package to create a signed OAuth request: | ||
@@ -739,3 +809,3 @@ ```js | ||
You can test your requests by using the [`nock`](https://github.com/node-nock/nock) module to mock an endpoint: | ||
You can test your requests by using the [`nock`](https://github.com/node-nock/nock) package to mock an endpoint: | ||
@@ -788,3 +858,3 @@ ```js | ||
headers: { | ||
'user-agent': `my-module/${pkg.version} (https://github.com/username/my-module)` | ||
'user-agent': `my-package/${pkg.version} (https://github.com/username/my-package)` | ||
} | ||
@@ -818,3 +888,3 @@ }); | ||
headers: { | ||
'user-agent': `my-module/${pkg.version} (https://github.com/username/my-module)` | ||
'user-agent': `my-package/${pkg.version} (https://github.com/username/my-package)` | ||
} | ||
@@ -831,33 +901,52 @@ }); | ||
### Experimental HTTP2 support | ||
Got provides an experimental support for HTTP2 using the [`http2-wrapper`](https://github.com/szmarczak/http2-wrapper) package: | ||
```js | ||
const got = require('got'); | ||
const {request} = require('http2-wrapper'); | ||
const h2got = got.extend({request}); | ||
(async () => { | ||
const {body} = await h2got('https://nghttp2.org/httpbin/headers'); | ||
console.log(body); | ||
})(); | ||
``` | ||
## Comparison | ||
| | `got` | `request` | `node-fetch` | `axios` | | ||
|-----------------------|:-------:|:---------:|:------------:|:-------:| | ||
| HTTP/2 support | ✖ | ✖ | ✖ | ✖ | | ||
| Browser support | ✖ | ✖ | ✔* | ✔ | | ||
| Electron support | ✔ | ✖ | ✖ | ✖ | | ||
| Promise API | ✔ | ✔ | ✔ | ✔ | | ||
| Stream API | ✔ | ✔ | ✖ | ✖ | | ||
| Request cancelation | ✔ | ✖ | ✖ | ✔ | | ||
| RFC compliant caching | ✔ | ✖ | ✖ | ✖ | | ||
| Follows redirects | ✔ | ✔ | ✔ | ✔ | | ||
| Retries on failure | ✔ | ✖ | ✖ | ✖ | | ||
| Progress events | ✔ | ✖ | ✖ | ✔ | | ||
| Handles gzip/deflate | ✔ | ✔ | ✔ | ✔ | | ||
| Advanced timeouts | ✔ | ✖ | ✖ | ✖ | | ||
| Errors with metadata | ✔ | ✖ | ✖ | ✔ | | ||
| JSON mode | ✔ | ✖ | ✖ | ✔ | | ||
| Custom defaults | ✔ | ✔ | ✖ | ✔ | | ||
| Composable | ✔ | ✖ | ✖ | ✖ | | ||
| Hooks | ✔ | ✖ | ✖ | ✔ | | ||
| Issues open | ![][gio] | ![][rio] | ![][nio] | ![][aio] | | ||
| Issues closed | ![][gic] | ![][ric] | ![][nic] | ![][aic] | | ||
| Downloads | ![][gd] | ![][rd] | ![][nd] | ![][ad] | | ||
| Coverage | ![][gc] | ![][rc] | ![][nc] | ![][ac] | | ||
| Build | ![][gb] | ![][rb] | ![][nb] | ![][ab] | | ||
| Dependents | ![][gdp] | ![][rdp] | ![][ndp] | ![][adp] | | ||
| Install size | ![][gis] | ![][ris] | ![][nis] | ![][ais] | | ||
| | `got` | `request` | `node-fetch` | `axios` | | ||
|-----------------------|:------------:|:------------:|:------------:|:------------:| | ||
| HTTP/2 support | ❔ | ✖ | ✖ | ✖ | | ||
| Browser support | ✖ | ✖ | ✔* | ✔ | | ||
| Electron support | ✔ | ✖ | ✖ | ✖ | | ||
| Promise API | ✔ | ✔ | ✔ | ✔ | | ||
| Stream API | ✔ | ✔ | ✖ | ✖ | | ||
| Request cancelation | ✔ | ✖ | ✖ | ✔ | | ||
| RFC compliant caching | ✔ | ✖ | ✖ | ✖ | | ||
| Cookies (out-of-box) | ✔ | ✔ | ✖ | ✖ | | ||
| Follows redirects | ✔ | ✔ | ✔ | ✔ | | ||
| Retries on failure | ✔ | ✖ | ✖ | ✖ | | ||
| Progress events | ✔ | ✖ | ✖ | Browser only | | ||
| Handles gzip/deflate | ✔ | ✔ | ✔ | ✔ | | ||
| Advanced timeouts | ✔ | ✖ | ✖ | ✖ | | ||
| Timings | ✔ | ✔ | ✖ | ✖ | | ||
| Errors with metadata | ✔ | ✖ | ✖ | ✔ | | ||
| JSON mode | ✔ | ✔ | ✖ | ✔ | | ||
| Custom defaults | ✔ | ✔ | ✖ | ✔ | | ||
| Composable | ✔ | ✖ | ✖ | ✖ | | ||
| Hooks | ✔ | ✖ | ✖ | ✔ | | ||
| Issues open | ![][gio] | ![][rio] | ![][nio] | ![][aio] | | ||
| Issues closed | ![][gic] | ![][ric] | ![][nic] | ![][aic] | | ||
| Downloads | ![][gd] | ![][rd] | ![][nd] | ![][ad] | | ||
| Coverage | ![][gc] | ![][rc] | ![][nc] | ![][ac] | | ||
| Build | ![][gb] | ![][rb] | ![][nb] | ![][ab] | | ||
| Bugs | ![][gbg] | ![][rbg] | ![][nbg] | ![][abg] | | ||
| Dependents | ![][gdp] | ![][rdp] | ![][ndp] | ![][adp] | | ||
| Install size | ![][gis] | ![][ris] | ![][nis] | ![][ais] | | ||
\* It's almost API compatible with the browser `fetch` API. | ||
\* It's almost API compatible with the browser `fetch` API.<br> | ||
❔ Experimental support. | ||
@@ -894,2 +983,8 @@ <!-- ISSUES OPEN --> | ||
<!-- BUGS --> | ||
[gbg]: https://badgen.net/github/label-issues/sindresorhus/got/bug/open | ||
[rbg]: https://badgen.net/github/label-issues/request/request/Needs%20investigation/open | ||
[nbg]: https://badgen.net/github/label-issues/bitinn/node-fetch/bug/open | ||
[abg]: https://badgen.net/github/label-issues/axios/axios/bug/open | ||
<!-- DEPENDENTS --> | ||
@@ -896,0 +991,0 @@ [gdp]: https://badgen.net/npm/dependents/got |
@@ -12,3 +12,3 @@ 'use strict'; | ||
const cancelable = new PCancelable((resolve, reject, onCancel) => { | ||
const promise = new PCancelable((resolve, reject, onCancel) => { | ||
const emitter = requestAsEventEmitter(options); | ||
@@ -24,2 +24,3 @@ let cancelOnRequest = false; | ||
request.abort(); | ||
return; | ||
} | ||
@@ -104,6 +105,2 @@ | ||
const promise = cancelable; | ||
promise.cancel = cancelable.cancel.bind(cancelable); | ||
promise.on = (name, fn) => { | ||
@@ -110,0 +107,0 @@ proxy.on(name, fn); |
@@ -31,6 +31,5 @@ 'use strict'; | ||
try { | ||
options = mergeOptions(defaults.options, options); | ||
return defaults.handler(normalizeArguments(url, options, defaults), next); | ||
} catch (error) { | ||
if (options.stream) { | ||
if (options && options.stream) { | ||
throw error; | ||
@@ -44,3 +43,3 @@ } else { | ||
got.create = create; | ||
got.extend = (options = {}) => create({ | ||
got.extend = options => create({ | ||
options: mergeOptions(defaults.options, options), | ||
@@ -53,5 +52,3 @@ handler: defaults.handler | ||
got.stream = (url, options) => { | ||
options = mergeOptions(defaults.options, options); | ||
options.stream = true; | ||
return defaults.handler(normalizeArguments(url, options, defaults), next); | ||
return defaults.handler(normalizeArguments(url, {...options, stream: true}, defaults), next); | ||
}; | ||
@@ -58,0 +55,0 @@ |
'use strict'; | ||
/* istanbul ignore next: compatibility reason */ | ||
const URLGlobal = typeof URL === 'undefined' ? require('url').URL : URL; // TODO: Use the `URL` global when targeting Node.js 10 | ||
/* istanbul ignore next: compatibility reason */ | ||
const URLSearchParamsGlobal = typeof URLSearchParams === 'undefined' ? require('url').URLSearchParams : URLSearchParams; | ||
const {URL, URLSearchParams} = require('url'); // TODO: Use the `URL` global when targeting Node.js 10 | ||
const is = require('@sindresorhus/is'); | ||
@@ -13,2 +10,3 @@ const toReadableStream = require('to-readable-stream'); | ||
const knownHookEvents = require('./known-hook-events'); | ||
const merge = require('./merge'); | ||
@@ -61,2 +59,4 @@ const retryAfterStatusCodes = new Set([413, 429, 503]); | ||
module.exports = (url, options, defaults) => { | ||
options = merge({}, defaults.options, options ? preNormalize(options) : {}); | ||
if (Reflect.has(options, 'url') || (is.object(url) && Reflect.has(url, 'url'))) { | ||
@@ -70,4 +70,2 @@ throw new TypeError('Parameter `url` is not an option. Use got(url, options)'); | ||
options = preNormalize(options); | ||
if (is.string(url)) { | ||
@@ -79,3 +77,3 @@ if (options.baseUrl) { | ||
url = urlToOptions(new URLGlobal(url, options.baseUrl)); | ||
url = urlToOptions(new URL(url, options.baseUrl)); | ||
} else { | ||
@@ -115,5 +113,7 @@ url = url.replace(/^unix:/, 'http://$&'); | ||
const {query} = options; | ||
if (!is.empty(query) || query instanceof URLSearchParamsGlobal) { | ||
const queryParams = new URLSearchParamsGlobal(query); | ||
options.path = `${options.path.split('?')[0]}?${queryParams.toString()}`; | ||
if (!is.empty(query) || query instanceof URLSearchParams) { | ||
if (!is.string(query)) { | ||
options.query = (new URLSearchParams(query)).toString(); | ||
} | ||
options.path = `${options.path.split('?')[0]}?${options.query}`; | ||
delete options.query; | ||
@@ -159,3 +159,3 @@ } | ||
headers['content-type'] = headers['content-type'] || 'application/x-www-form-urlencoded'; | ||
options.body = (new URLSearchParamsGlobal(body)).toString(); | ||
options.body = (new URLSearchParams(body)).toString(); | ||
} else if (options.json) { | ||
@@ -162,0 +162,0 @@ headers['content-type'] = headers['content-type'] || 'application/json'; |
'use strict'; | ||
module.exports = { | ||
@@ -31,8 +32,2 @@ upload(request, emitter, uploadBodySize) { | ||
progressInterval = setInterval(() => { | ||
/* istanbul ignore next: hard to test */ | ||
if (socket.destroyed) { | ||
clearInterval(progressInterval); | ||
return; | ||
} | ||
const lastUploaded = uploaded; | ||
@@ -63,5 +58,6 @@ /* istanbul ignore next: see #490 (occurs randomly!) */ | ||
/* istanbul ignore next: hard to test */ | ||
if (socket.connecting) { | ||
socket.once('connect', onSocketConnect); | ||
} else { | ||
} else if (socket.writable) { | ||
// The socket is being reused from pool, | ||
@@ -68,0 +64,0 @@ // so the connect event will not be emitted |
'use strict'; | ||
/* istanbul ignore next: compatibility reason */ | ||
const URLGlobal = typeof URL === 'undefined' ? require('url').URL : URL; // TODO: Use the `URL` global when targeting Node.js 10 | ||
const {URL} = require('url'); // TODO: Use the `URL` global when targeting Node.js 10 | ||
const util = require('util'); | ||
const EventEmitter = require('events'); | ||
@@ -10,2 +10,3 @@ const http = require('http'); | ||
const is = require('@sindresorhus/is'); | ||
const timer = require('@szmarczak/http-timer'); | ||
const timedOut = require('./timed-out'); | ||
@@ -22,3 +23,3 @@ const getBodySize = require('./get-body-size'); | ||
const emitter = new EventEmitter(); | ||
const requestUrl = options.href || (new URLGlobal(options.path, urlLib.format(options))).toString(); | ||
const requestUrl = options.href || (new URL(options.path, urlLib.format(options))).toString(); | ||
const redirects = []; | ||
@@ -31,3 +32,8 @@ const agents = is.object(options.agent) ? options.agent : null; | ||
const get = options => { | ||
const setCookie = options.cookieJar ? util.promisify(options.cookieJar.setCookie.bind(options.cookieJar)) : null; | ||
const getCookieString = options.cookieJar ? util.promisify(options.cookieJar.getCookieString.bind(options.cookieJar)) : null; | ||
const get = async options => { | ||
const currentUrl = redirectUrl || requestUrl; | ||
if (options.protocol !== 'http:' && options.protocol !== 'https:') { | ||
@@ -38,3 +44,8 @@ emitter.emit('error', new UnsupportedProtocolError(options)); | ||
let fn = options.protocol === 'https:' ? https : http; | ||
let fn; | ||
if (is.function(options.request)) { | ||
fn = {request: options.request}; | ||
} else { | ||
fn = options.protocol === 'https:' ? https : http; | ||
} | ||
@@ -48,13 +59,51 @@ if (agents) { | ||
if (options.useElectronNet && process.versions.electron) { | ||
const electron = global['require']('electron'); // eslint-disable-line dot-notation | ||
const r = ({x: require})['yx'.slice(1)]; // Trick webpack | ||
const electron = r('electron'); | ||
fn = electron.net || electron.remote.net; | ||
} | ||
if (options.cookieJar) { | ||
try { | ||
const cookieString = await getCookieString(currentUrl, {}); | ||
if (!is.empty(cookieString)) { | ||
options.headers.cookie = cookieString; | ||
} | ||
} catch (error) { | ||
emitter.emit('error', error); | ||
} | ||
} | ||
let timings; | ||
const cacheableRequest = new CacheableRequest(fn.request, options.cache); | ||
const cacheReq = cacheableRequest(options, response => { | ||
const cacheReq = cacheableRequest(options, async response => { | ||
/* istanbul ignore next: fixes https://github.com/electron/electron/blob/cbb460d47628a7a146adf4419ed48550a98b2923/lib/browser/api/net.js#L59-L65 */ | ||
if (options.useElectronNet) { | ||
response = new Proxy(response, { | ||
get: (target, name) => { | ||
if (name === 'trailers' || name === 'rawTrailers') { | ||
return []; | ||
} | ||
const value = target[name]; | ||
return is.function(value) ? value.bind(target) : value; | ||
} | ||
}); | ||
} | ||
const {statusCode} = response; | ||
response.url = currentUrl; | ||
response.requestUrl = requestUrl; | ||
response.retryCount = retryCount; | ||
response.url = redirectUrl || requestUrl; | ||
response.requestUrl = requestUrl; | ||
response.timings = timings; | ||
const rawCookies = response.headers['set-cookie']; | ||
if (options.cookieJar && rawCookies) { | ||
try { | ||
await Promise.all(rawCookies.map(rawCookie => setCookie(rawCookie, response.url))); | ||
} catch (error) { | ||
emitter.emit('error', error); | ||
} | ||
} | ||
const followRedirect = options.followRedirect && 'location' in response.headers; | ||
@@ -79,3 +128,3 @@ const redirectGet = followRedirect && getMethodRedirectCodes.has(statusCode); | ||
const bufferString = Buffer.from(response.headers.location, 'binary').toString(); | ||
redirectUrl = (new URLGlobal(bufferString, urlLib.format(options))).toString(); | ||
redirectUrl = (new URL(bufferString, urlLib.format(options))).toString(); | ||
@@ -98,3 +147,3 @@ try { | ||
get(redirectOpts); | ||
await get(redirectOpts); | ||
return; | ||
@@ -139,2 +188,4 @@ } | ||
timings = timer(request); | ||
progress.upload(request, emitter, uploadBodySize); | ||
@@ -173,8 +224,6 @@ | ||
if ( | ||
uploadBodySize > 0 && | ||
is.undefined(options.headers['content-length']) && | ||
is.undefined(options.headers['transfer-encoding']) | ||
) { | ||
options.headers['content-length'] = uploadBodySize; | ||
if (is.undefined(options.headers['content-length']) && is.undefined(options.headers['transfer-encoding'])) { | ||
if (uploadBodySize > 0 || options.method === 'PUT') { | ||
options.headers['content-length'] = uploadBodySize; | ||
} | ||
} | ||
@@ -187,3 +236,3 @@ | ||
get(options); | ||
await get(options); | ||
} catch (error) { | ||
@@ -190,0 +239,0 @@ emitter.emit('error', error); |
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
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
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
73350
1158
1006
10
15
3
+ Added@szmarczak/http-timer@^1.1.0
+ Added@szmarczak/http-timer@1.1.2(transitive)
+ Addeddefer-to-connect@1.1.3(transitive)