http2-wrapper
HTTP/2 client, just with the familiar
httpsAPI
This package was created to support HTTP/2 without the need to
rewrite your code.
I recommend adapting to the http2 module
if possible - it’s much simpler to use and has many cool features!
Tip: http2-wrapper is very useful when
you rely on other modules that use the HTTP/1 API and you want to
support HTTP/2.
Pro Tip: While the native http2 doesn’t
have agents yet, you can use http2-wrapper Agents and still
operate on the native HTTP/2 streams.
Installation
$ npm install http2-wrapper
$ yarn add http2-wrapper
Usage
const http2 = require('http2-wrapper');
const options = {
hostname: 'nghttp2.org',
protocol: 'https:',
path: '/httpbin/post',
method: 'POST',
headers: {
'content-length': 6
}
};
const request = http2.request(options, response => {
console.log('statusCode:', response.statusCode);
console.log('headers:', response.headers);
const body = [];
response.on('data', chunk => {
body.push(chunk);
});
response.on('end', () => {
console.log('body:', Buffer.concat(body).toString());
});
});
request.on('error', console.error);
request.write('123');
request.end('456');
// statusCode: 200
// headers: [Object: null prototype] {
// ':status': 200,
// date: 'Fri, 27 Sep 2019 19:45:46 GMT',
// 'content-type': 'application/json',
// 'access-control-allow-origin': '*',
// 'access-control-allow-credentials': 'true',
// 'content-length': '239',
// 'x-backend-header-rtt': '0.002516',
// 'strict-transport-security': 'max-age=31536000',
// server: 'nghttpx',
// via: '1.1 nghttpx',
// 'alt-svc': 'h3-23=":4433"; ma=3600',
// 'x-frame-options': 'SAMEORIGIN',
// 'x-xss-protection': '1; mode=block',
// 'x-content-type-options': 'nosniff'
// }
// body: {
// "args": {},
// "data": "123456",
// "files": {},
// "form": {},
// "headers": {
// "Content-Length": "6",
// "Host": "nghttp2.org"
// },
// "json": 123456,
// "origin": "xxx.xxx.xxx.xxx",
// "url": "https://nghttp2.org/httpbin/post"
// }API
Note: The session option was renamed to
tlsSession for better readability.
http2.auto(url, options, callback)
Performs ALPN
negotiation. Returns a Promise giving proper ClientRequest
instance (depending on the ALPN).
Note: The agent option represents an
object with http, https and http2
properties.
const http2 = require('http2-wrapper');
const options = {
hostname: 'httpbin.org',
protocol: 'http:', // Note the `http:` protocol here
path: '/post',
method: 'POST',
headers: {
'content-length': 6
}
};
(async () => {
try {
const request = await http2.auto(options, response => {
console.log('statusCode:', response.statusCode);
console.log('headers:', response.headers);
const body = [];
response.on('data', chunk => body.push(chunk));
response.on('end', () => {
console.log('body:', Buffer.concat(body).toString());
});
});
request.on('error', console.error);
request.write('123');
request.end('456');
} catch (error) {
console.error(error);
}
})();
// statusCode: 200
// headers: { connection: 'close',
// server: 'gunicorn/19.9.0',
// date: 'Sat, 15 Dec 2018 18:19:32 GMT',
// 'content-type': 'application/json',
// 'content-length': '259',
// 'access-control-allow-origin': '*',
// 'access-control-allow-credentials': 'true',
// via: '1.1 vegur' }
// body: {
// "args": {},
// "data": "123456",
// "files": {},
// "form": {},
// "headers": {
// "Connection": "close",
// "Content-Length": "6",
// "Host": "httpbin.org"
// },
// "json": 123456,
// "origin": "xxx.xxx.xxx.xxx",
// "url": "http://httpbin.org/post"
// }http2.auto.protocolCache
An instance of quick-lru
used for ALPN cache.
There is a maximum of 100 entries. You can modify the limit through
protocolCache.maxSize - note that the change will be
visible globally.
http2.request(url, options, callback)
Same as https.request.
options.h2session
Type: Http2Session
The session used to make the actual request. If none provided, it
will use options.agent.
http2.get(url, options, callback)
Same as https.get.
new http2.ClientRequest(url, options, callback)
Same as https.ClientRequest.
new http2.IncomingMessage(socket)
Same as https.IncomingMessage.
new http2.Agent(options)
Note: this is not compatible with
the classic http.Agent.
Usage example:
const http2 = require('http2-wrapper');
class MyAgent extends http2.Agent {
createConnection(origin, options) {
console.log(`Connecting to ${http2.Agent.normalizeOrigin(origin)}`);
return http2.Agent.connect(origin, options);
}
}
http2.get({
hostname: 'google.com',
agent: new MyAgent()
}, res => {
res.on('data', chunk => console.log(`Received chunk of ${chunk.length} bytes`));
});options
Each option is assigned to each Agent instance and can
be changed later.
timeout
Type: number
Default: 60000
If there’s no activity after timeout milliseconds, the
session will be closed.
maxSessions
Type: number
Default: Infinity
The maximum amount of sessions in total.
maxFreeSessions
Type: number
Default: 10
The maximum amount of free sessions in total. This only applies to sessions with no pending requests.
Note: It is possible that the amount will be exceeded when sessions have at least 1 pending request.
maxCachedTlsSessions
Type: number
Default: 100
The maximum amount of cached TLS sessions.
Agent.normalizeOrigin(url)
Returns a string representing the origin of the URL.
agent.settings
Type: object
Default:
{enablePush: false}
Settings used by the current agent instance.
agent.normalizeOptions(options)
Returns a string representing normalized options.
Agent.normalizeOptions({servername: 'example.com'});
// => ':example.com'agent.getSession(origin, options)
origin
Type: string URL object
An origin used to create new session.
options
Type: object
The options used to create new session.
Returns a Promise giving free Http2Session. If no free
sessions are found, a new one is created.
agent.getSession(origin, options, listener)
listener
Type: object
{
reject: error => void,
resolve: session => void
}
If the listener argument is present, the Promise will
resolve immediately. It will use the resolve function to
pass the session.
agent.request(origin, options, headers, streamOptions)
Returns a Promise giving Http2Stream.
agent.createConnection(origin, options)
Returns a new TLSSocket. It defaults to
Agent.connect(origin, options).
agent.closeFreeSessions()
Makes an attempt to close free sessions. Only sessions with 0 concurrent streams will be closed.
agent.destroy(reason)
Destroys all sessions.
Event: ‘session’
agent.on('session', session => {
// A new session has been created by the Agent.
});Proxy support
An example of a full-featured proxy server can be found here. It supports mirroring, custom authorities and the CONNECT protocol.
Mirroring
To mirror another server we need to use only http2-proxy.
We don’t need the CONNECT protocol or custom authorities.
To see the result, just navigate to the server’s address.
HTTP/1 over HTTP/2
Since we don’t care about mirroring, the server needs to support the CONNECT protocol in this case.
The client looks like this:
const https = require('https');
const http2 = require('http2');
const session = http2.connect('https://localhost:8000', {
// For demo purposes only!
rejectUnauthorized: false
});
session.ref();
https.request('https://httpbin.org/anything', {
createConnection: options => {
return session.request({
':method': 'CONNECT',
':authority': `${options.host}:${options.port}`
});
}
}, response => {
console.log('statusCode:', response.statusCode);
console.log('headers:', response.headers);
const body = [];
response.on('data', chunk => {
body.push(chunk);
});
response.on('end', () => {
console.log('body:', Buffer.concat(body).toString());
session.unref();
});
}).end();HTTP/2 over HTTP/2
It’s a tricky one! We cannot create an HTTP/2 session on top of an
HTTP/2 stream. But… we can still specify the :authority
header, no need to use the CONNECT protocol here.
The client looks like this:
const http2 = require('../../source');
const {Agent} = http2;
class ProxyAgent extends Agent {
constructor(url, options) {
super(options);
this.origin = url;
}
request(origin, sessionOptions, headers, streamOptions) {
return super.request(this.origin, sessionOptions, {
...headers,
':authority': (new URL(origin)).host
}, streamOptions);
}
}
const request = http2.request({
hostname: 'httpbin.org',
protocol: 'https:',
path: '/anything',
agent: new ProxyAgent('https://localhost:8000'),
// For demo purposes only!
rejectUnauthorized: false
}, response => {
console.log('statusCode:', response.statusCode);
console.log('headers:', response.headers);
const body = [];
response.on('data', chunk => {
body.push(chunk);
});
response.on('end', () => {
console.log('body:', Buffer.concat(body).toString());
});
});
request.on('error', console.error);
request.end();Notes
- If you’re interested in WebSockets over HTTP/2, then check out this discussion.
- HTTP/2 sockets cannot be malformed, therefore modifying the socket will have no effect.
- You can make a custom Agent to support push streams.
Benchmarks
CPU: Intel i7-7700k (governor: performance)
Server: H2O v2.2.5 h2o.conf
Node: v14.5.0 Linux:
5.6.18-156.current
auto means http2wrapper.auto.
http2-wrapper x 12,181 ops/sec ±3.39% (75 runs sampled)
http2-wrapper - preconfigured session x 13,140 ops/sec ±2.51% (79 runs sampled)
http2-wrapper - auto x 11,412 ops/sec ±2.55% (78 runs sampled)
http2 x 16,050 ops/sec ±1.39% (86 runs sampled)
https - auto - keepalive x 12,288 ops/sec ±2.69% (79 runs sampled)
https - keepalive x 12,155 ops/sec ±3.32% (78 runs sampled)
https x 1,604 ops/sec ±2.03% (77 runs sampled)
http x 6,041 ops/sec ±3.82% (76 runs sampled)
Fastest is http2
http2-wrapper: - 32% less performant
than http2 - as performant as
https - keepalive - 100% more performant
than http
http2-wrapper - preconfigured session: - 22%
less performant than http2 - 8%
more performant than https - keepalive -
118% more performant than http
http2-wrapper - auto: - 41% less
performant than http2 - 8% less performant
than https - keepalive - 89% more
performant than http
https - auto - keepalive: - 31% less
performant than http2 - as performant as
https - keepalive - 103% more performant
than http
Related
got- Simplified HTTP requests
License
MIT