Skip to content

Commit f560b4d

Browse files
authored
feat: allow passing a http.Agent to ipfs-http-client in node (#3474)
Right now no `http.Agent` is used for requests made using the http client in node, which means each request opens a new connection which can end up hitting process resource limits which means connections get dropped. The change here sets a default `http.Agent` with a `keepAlive: true` and `maxSockets` of 6 which is consistent with [browsers](https://tools.ietf.org/html/rfc2616#section-8.1.4) and [native apps](https://developer.apple.com/documentation/foundation/nsurlsessionconfiguration/1407597-httpmaximumconnectionsperhost?language=objc). The user can override the agent passed to the `ipfs-http-client` constructor to restore the previous functionality: ```js const http = require('http') const createClient = require('ipfs-http-client') const client = createClient({ url: 'http://127.0.0.1:5002', agent: new http.Agent({ keepAlive: false, maxSockets: Infinity }) }) ``` Refs: #3464
1 parent ebc1dfa commit f560b4d

File tree

5 files changed

+134
-4
lines changed

5 files changed

+134
-4
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ All core API methods take _additional_ `options` specific to the HTTP API:
9999

100100
* `headers` - An object or [Headers](https://developer.mozilla.org/en-US/docs/Web/API/Headers) instance that can be used to set custom HTTP headers. Note that this option can also be [configured globally](#custom-headers) via the constructor options.
101101
* `searchParams` - An object or [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) instance that can be used to add additional query parameters to the query string sent with each request.
102+
* `agent` - A node [http.Agent](https://nodejs.org/api/http.html#http_class_http_agent) used to configure connection persistence and reuse (only supported in node.js)
102103

103104
### Instance Utils
104105

package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
"./src/lib/multipart-request.js": "./src/lib/multipart-request.browser.js",
1919
"ipfs-utils/src/files/glob-source": false,
2020
"go-ipfs": false,
21-
"ipfs-core-utils/src/files/normalise-input": "ipfs-core-utils/src/files/normalise-input/index.browser.js"
21+
"ipfs-core-utils/src/files/normalise-input": "ipfs-core-utils/src/files/normalise-input/index.browser.js",
22+
"http": false
2223
},
2324
"typesVersions": {
2425
"*": {
@@ -79,6 +80,7 @@
7980
},
8081
"devDependencies": {
8182
"aegir": "^29.2.2",
83+
"delay": "^4.4.0",
8284
"go-ipfs": "^0.7.0",
8385
"ipfs-core": "^0.3.1",
8486
"ipfsd-ctl": "^7.2.0",

src/lib/core.js

+18-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
'use strict'
22
/* eslint-env browser */
33
const Multiaddr = require('multiaddr')
4-
const { isBrowser, isWebWorker } = require('ipfs-utils/src/env')
4+
const { isBrowser, isWebWorker, isNode } = require('ipfs-utils/src/env')
55
const parseDuration = require('parse-duration').default
66
const log = require('debug')('ipfs-http-client:lib:error-handler')
77
const HTTP = require('ipfs-utils/src/http')
88
const merge = require('merge-options')
99
const toUrlString = require('ipfs-core-utils/src/to-url-string')
10+
const http = require('http')
1011

1112
const DEFAULT_PROTOCOL = isBrowser || isWebWorker ? location.protocol : 'http'
1213
const DEFAULT_HOST = isBrowser || isWebWorker ? location.hostname : 'localhost'
@@ -19,6 +20,7 @@ const DEFAULT_PORT = isBrowser || isWebWorker ? location.port : '5001'
1920
const normalizeOptions = (options = {}) => {
2021
let url
2122
let opts = {}
23+
let agent
2224

2325
if (typeof options === 'string' || Multiaddr.isMultiaddr(options)) {
2426
url = new URL(toUrlString(options))
@@ -46,13 +48,22 @@ const normalizeOptions = (options = {}) => {
4648
url.pathname = 'api/v0'
4749
}
4850

51+
if (isNode) {
52+
agent = opts.agent || new http.Agent({
53+
keepAlive: true,
54+
// Similar to browsers which limit connections to six per host
55+
maxSockets: 6
56+
})
57+
}
58+
4959
return {
5060
...opts,
5161
host: url.host,
5262
protocol: url.protocol.replace(':', ''),
5363
port: Number(url.port),
5464
apiPath: url.pathname,
55-
url
65+
url,
66+
agent
5667
}
5768
}
5869

@@ -105,6 +116,8 @@ const parseTimeout = (value) => {
105116
}
106117

107118
/**
119+
* @typedef {import('http').Agent} Agent
120+
*
108121
* @typedef {Object} ClientOptions
109122
* @property {string} [host]
110123
* @property {number} [port]
@@ -116,6 +129,7 @@ const parseTimeout = (value) => {
116129
* @property {object} [ipld]
117130
* @property {any[]} [ipld.formats] - An array of additional [IPLD formats](https://github.com/ipld/interface-ipld-format) to support
118131
* @property {(format: string) => Promise<any>} [ipld.loadFormat] - an async function that takes the name of an [IPLD format](https://github.com/ipld/interface-ipld-format) as a string and should return the implementation of that codec
132+
* @property {Agent} [agent] - A [http.Agent](https://nodejs.org/api/http.html#http_class_http_agent) used to control connection persistence and reuse for HTTP clients (only supported in node.js)
119133
*/
120134
class Client extends HTTP {
121135
/**
@@ -149,7 +163,8 @@ class Client extends HTTP {
149163
}
150164

151165
return out
152-
}
166+
},
167+
agent: opts.agent
153168
})
154169

155170
delete this.get

test/node.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use strict'
22

3+
require('./node/agent')
34
require('./node/swarm')
45
require('./node/request-api')
56
require('./node/custom-headers')

test/node/agent.js

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/* eslint-env mocha */
2+
'use strict'
3+
4+
const { expect } = require('aegir/utils/chai')
5+
const ipfsClient = require('../../src')
6+
const delay = require('delay')
7+
8+
function startServer (handler) {
9+
return new Promise((resolve) => {
10+
// spin up a test http server to inspect the requests made by the library
11+
const server = require('http').createServer((req, res) => {
12+
req.on('data', () => {})
13+
req.on('end', async () => {
14+
const out = await handler(req)
15+
16+
res.writeHead(200)
17+
res.write(JSON.stringify(out))
18+
res.end()
19+
})
20+
})
21+
22+
server.listen(0, () => {
23+
resolve({
24+
port: server.address().port,
25+
close: () => server.close()
26+
})
27+
})
28+
})
29+
}
30+
31+
describe('agent', function () {
32+
let agent
33+
34+
before(() => {
35+
const { Agent } = require('http')
36+
37+
agent = new Agent({
38+
maxSockets: 2
39+
})
40+
})
41+
42+
it('restricts the number of concurrent connections', async () => {
43+
const responses = []
44+
45+
const server = await startServer(() => {
46+
const p = new Promise((resolve) => {
47+
responses.push(resolve)
48+
})
49+
50+
return p
51+
})
52+
53+
const ipfs = ipfsClient({
54+
url: `http://localhost:${server.port}`,
55+
agent
56+
})
57+
58+
// make three requests
59+
const requests = Promise.all([
60+
ipfs.id(),
61+
ipfs.id(),
62+
ipfs.id()
63+
])
64+
65+
// wait for the first two to arrive
66+
for (let i = 0; i < 5; i++) {
67+
await delay(100)
68+
69+
if (responses.length === 2) {
70+
// wait a little longer, the third should not arrive
71+
await delay(1000)
72+
73+
expect(responses).to.have.lengthOf(2)
74+
75+
// respond to the in-flight requests
76+
responses[0]({
77+
res: 0
78+
})
79+
responses[1]({
80+
res: 1
81+
})
82+
83+
break
84+
}
85+
}
86+
87+
// wait for the final request to arrive
88+
for (let i = 0; i < 5; i++) {
89+
await delay(100)
90+
91+
if (responses.length === 3) {
92+
// respond to it
93+
responses[2]({
94+
res: 2
95+
})
96+
}
97+
}
98+
99+
const results = await requests
100+
101+
expect(results).to.deep.equal([{
102+
res: 0
103+
}, {
104+
res: 1
105+
}, {
106+
res: 2
107+
}])
108+
109+
server.close()
110+
})
111+
})

0 commit comments

Comments
 (0)