Skip to content

Commit

Permalink
Use undici by default (#143)
Browse files Browse the repository at this point in the history
* Use undici by default

* Drop deprecated options & better defaults

* Increase code coverage

* Code coverage

* Minimum node version is v10.

* Support for unix sockets
  • Loading branch information
mcollina committed Feb 24, 2021
1 parent c72f1b5 commit ffb1a01
Show file tree
Hide file tree
Showing 16 changed files with 435 additions and 99 deletions.
77 changes: 31 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,36 @@ proxy.register(require('fastify-reply-from'), {
});
```

#### `undici`

By default, [undici](https://github.com/mcollina/undici) will be used to perform the HTTP/1.1
requests. Enabling this flag should guarantee
20-50% more throughput.

This flag could controls the settings of the undici client, like so:

```js
proxy.register(require('fastify-reply-from'), {
base: 'http://localhost:3001/',
// default settings
undici: {
connections: 128,
pipelining: 1,
keepAliveTimeout: 60 * 1000,
tls: {
rejectUnauthorized: false
}
}
})
```

See undici own options for more configurations.

#### `http`
By default, Node's [`http.request`](https://nodejs.org/api/http.html#http_http_request_options_callback)
will be used if you do not enable [`http2`](#http2) or [`undici`](#undici). To customize the `request`,

Set the `http` option to `true` or to an Object to use
Node's [`http.request`](https://nodejs.org/api/http.html#http_http_request_options_callback)
will be used if you do not enable [`http2`](#http2). To customize the `request`,
you can pass in [`agentOptions`](https://nodejs.org/api/http.html#http_new_agent_options) and
[`requestOptions`](https://nodejs.org/api/http.html#http_http_request_options_callback). To illustrate:

Expand Down Expand Up @@ -118,7 +145,9 @@ proxy.register(require('fastify-reply-from'), {
}
})
```

#### `http2`

You can either set `http2` to `true` or set the settings object to connect to a HTTP/2 server.
The `http2` settings object has the shape of:

Expand All @@ -138,54 +167,10 @@ proxy.register(require('fastify-reply-from'), {
})
```

#### `undici`
Set to `true` to use [undici](https://github.com/mcollina/undici)
instead of `require('http')`. Enabling this flag should guarantee
20-50% more throughput.

This flag could controls the settings of the undici client, like so:

```js
proxy.register(require('fastify-reply-from'), {
base: 'http://localhost:3001/',
undici: {
connections: 100,
pipelining: 10
}
})
```

#### `cacheURLs`

The number of parsed URLs that will be cached. Default: `100`.

#### `keepAliveMsecs`

**(Deprecated)** Defaults to 1 minute (`60000`), passed down to [`http.Agent`][http-agent] and
[`https.Agent`][https-agent] instances. Prefer to use [`http.agentOptions`](#http) instead.

#### `maxSockets`

**(Deprecated)** Defaults to `2048` sockets, passed down to [`http.Agent`][http-agent] and
[`https.Agent`][https-agent] instances. Prefer to use [`http.agentOptions`](#http) instead.

#### `maxFreeSockets`

**(Deprecated)** Defaults to `2048` free sockets, passed down to [`http.Agent`][http-agent] and
[`https.Agent`][https-agent] instances. Prefer to use [`http.agentOptions`](#http) instead.

#### `rejectUnauthorized`

**(Deprecated)** Defaults to `false`, passed down to [`https.Agent`][https-agent] instances.
This needs to be set to `false` to reply from https servers with
self-signed certificates. Prefer to use [`http.requestOptions`](#http) or
[`http2.sessionOptions`](#http2) instead.

#### `sessionTimeout`

**(Deprecated)** The timeout value after which the HTTP2 client session is destroyed if there
is no activity. Defaults to 1 minute (`60000`). Prefer to use [`http2.sessionTimeout`](#http2) instead.

---

### `reply.from(source, [opts])`
Expand Down
5 changes: 0 additions & 5 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,6 @@ module.exports = fp(function from (fastify, opts, next) {
http: opts.http,
http2: opts.http2,
base,
keepAliveMsecs: opts.keepAliveMsecs,
maxFreeSockets: opts.maxFreeSockets,
maxSockets: opts.maxSockets,
rejectUnauthorized: opts.rejectUnauthorized,
sessionTimeout: opts.sessionTimeout,
undici: opts.undici
})
fastify.decorateReply('from', function (source, opts) {
Expand Down
79 changes: 52 additions & 27 deletions lib/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,20 @@ const eos = require('end-of-stream')
const pump = require('pump')
const undici = require('undici')
const { stripHttp1ConnectionHeaders } = require('./utils')
const http2 = require('http2')

class TimeoutError extends Error {}

function shouldUseUndici (opts) {
if (opts.undici === false || opts.http || opts.http2) {
return false
}
return true
}

function buildRequest (opts) {
const isHttp2 = !!opts.http2
const isUndici = !!opts.undici
const isUndici = shouldUseUndici(opts)
const requests = {
'http:': http,
'https:': https,
Expand All @@ -22,11 +30,11 @@ function buildRequest (opts) {
const baseUrl = opts.base
const http2Opts = getHttp2Opts(opts)
const httpOpts = getHttpOpts(opts)
const undiciOpts = opts.undici
const undiciOpts = opts.undici || {}
let http2Client
let pool
let undiciAgent
let undiciPool
let agents
let http2

if (isHttp2) {
if (semver.lt(process.version, '9.0.0')) {
Expand All @@ -44,24 +52,25 @@ function buildRequest (opts) {
}

if (isHttp2) {
http2 = getHttp2()
return { request: handleHttp2Req, close }
} else if (isUndici) {
if (opts.base.startsWith('unix+')) {
throw new Error('Unix socket destination is not supported when undici is enabled')
}
if (typeof opts.undici !== 'object') {
opts.undici = {}
if (opts.base && opts.base.startsWith('unix+')) {
const undiciOpts = getUndiciOptions(opts.undici)
undiciOpts.socketPath = decodeURIComponent(new URL(opts.base).host)
const protocol = opts.base.startsWith('unix+https') ? 'https' : 'http'
undiciPool = new undici.Pool(protocol + '://localhost', undiciOpts)
} else {
undiciAgent = new undici.Agent(getUndiciOptions(opts.undici))
}
pool = new undici.Pool(baseUrl, opts.undici)
return { request: handleUndici, close }
} else {
return { request: handleHttp1Req, close }
}

function close () {
if (isUndici) {
pool.destroy()
undiciAgent && undiciAgent.destroy()
undiciPool && undiciPool.destroy()
} else if (!isHttp2) {
agents['http:'].destroy()
agents['https:'].destroy()
Expand Down Expand Up @@ -102,6 +111,17 @@ function buildRequest (opts) {
bodyTimeout: undiciOpts.bodyTimeout
}

let pool

if (undiciPool) {
pool = undiciPool
} else if (!baseUrl && opts.url.protocol.startsWith('unix')) {
done(new Error('unix socket not supported with undici yet'))
return
} else {
pool = undiciAgent.get(baseUrl || opts.url.origin)
}

// remove forbidden headers
req.headers.connection = undefined
req.headers['transfer-encoding'] = undefined
Expand Down Expand Up @@ -195,11 +215,6 @@ function end (req, body, cb) {
}
}

// neede to avoid the experimental warning
function getHttp2 () {
return require('http2')
}

function getHttp2Opts (opts) {
if (!opts.http2) {
return {}
Expand All @@ -217,23 +232,20 @@ function getHttp2Opts (opts) {
if (!http2Opts.requestTimeout) {
http2Opts.requestTimeout = 10000
}
if (!opts.rejectUnauthorized) {
http2Opts.sessionOptions.rejectUnauthorized = false
}
http2Opts.sessionOptions.rejectUnauthorized = http2Opts.sessionOptions.rejectUnauthorized || false

return http2Opts
}

function getHttpOpts (opts) {
const httpOpts = opts.http || {}
const httpOpts = typeof opts.http === 'object' ? opts.http : {}
httpOpts.requestOptions = httpOpts.requestOptions || {}

if (!httpOpts.requestOptions.timeout) {
httpOpts.requestOptions.timeout = 10000
}
if (!opts.rejectUnauthorized) {
httpOpts.requestOptions.rejectUnauthorized = false
}

httpOpts.requestOptions.rejectUnauthorized = httpOpts.requestOptions.rejectUnauthorized || false

httpOpts.agentOptions = getAgentOptions(opts)

Expand All @@ -243,9 +255,22 @@ function getHttpOpts (opts) {
function getAgentOptions (opts) {
return {
keepAlive: true,
keepAliveMsecs: opts.keepAliveMsecs || 60 * 1000, // 1 minute
maxSockets: opts.maxSockets || 2048,
maxFreeSockets: opts.maxFreeSockets || 2048,
keepAliveMsecs: 60 * 1000, // 1 minute
maxSockets: 2048,
maxFreeSockets: 2048,
...(opts.http && opts.http.agentOptions)
}
}

function getUndiciOptions (opts = {}) {
const res = {
pipelining: 1,
connections: 128,
tls: {},
...(opts)
}

res.tls.rejectUnauthorized = res.tls.rejectUnauthorized || false

return res
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"url": "https://github.com/fastify/fastify-reply-from/issues"
},
"engines": {
"node": ">=8.0.0"
"node": ">=10.0.0"
},
"homepage": "https://github.com/fastify/fastify-reply-from#readme",
"devDependencies": {
Expand Down
4 changes: 3 additions & 1 deletion test/base-path.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ instance.get('/', (request, reply) => {
reply.from('http://httpbin.org/ip')
})

instance.register(From)
instance.register(From, {
undici: false
})

instance.listen(0, (err) => {
t.error(err)
Expand Down
66 changes: 66 additions & 0 deletions test/full-post-stream-core.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
'use strict'

const t = require('tap')
const Fastify = require('fastify')
const From = require('..')
const http = require('http')
const get = require('simple-get').concat

const instance = Fastify()
instance.register(From, {
http: true
})

t.plan(8)
t.tearDown(instance.close.bind(instance))

instance.addContentTypeParser('application/octet-stream', function (req, payload, done) {
done(null, payload)
})

t.tearDown(instance.close.bind(instance))

const target = http.createServer((req, res) => {
t.pass('request proxied')
t.equal(req.method, 'POST')
t.equal(req.headers['content-type'], 'application/octet-stream')
let data = ''
req.setEncoding('utf8')
req.on('data', (d) => {
data += d
})
req.on('end', () => {
t.deepEqual(JSON.parse(data), { hello: 'world' })
res.statusCode = 200
res.setHeader('content-type', 'application/octet-stream')
res.end(JSON.stringify({ something: 'else' }))
})
})

instance.post('/', (request, reply) => {
reply.from(`http://localhost:${target.address().port}`)
})

t.tearDown(target.close.bind(target))

instance.listen(0, (err) => {
t.error(err)

target.listen(0, (err) => {
t.error(err)

get({
url: `http://localhost:${instance.server.address().port}`,
method: 'POST',
headers: {
'content-type': 'application/octet-stream'
},
body: JSON.stringify({
hello: 'world'
})
}, (err, res, data) => {
t.error(err)
t.deepEqual(JSON.parse(data), { something: 'else' })
})
})
})
Loading

0 comments on commit ffb1a01

Please sign in to comment.