http2-wrapper
Advanced tools
Comparing version 0.4.2 to 0.5.0
{ | ||
"name": "http2-wrapper", | ||
"version": "0.4.2", | ||
"description": "HTTP2 client, but with the HTTP1 API", | ||
"version": "0.5.0", | ||
"description": "HTTP2 client, just with the familiar `https` API", | ||
"main": "source", | ||
@@ -17,3 +17,6 @@ "engines": { | ||
"keywords": [ | ||
"http2" | ||
"http2", | ||
"https", | ||
"http", | ||
"request" | ||
], | ||
@@ -30,7 +33,11 @@ "repository": { | ||
"homepage": "https://github.com/szmarczak/http2-wrapper#readme", | ||
"dependencies": { | ||
"resolve-alpn": "^1.0.0" | ||
}, | ||
"devDependencies": { | ||
"@sindresorhus/is": "^0.14.0", | ||
"ava": "^1.0.1", | ||
"coveralls": "^3.0.2", | ||
"delay": "^4.1.0", | ||
"benchmark": "^2.1.4", | ||
"coveralls": "^3.0.4", | ||
"delay": "^4.3.0", | ||
"get-stream": "^4.0.0", | ||
@@ -40,7 +47,10 @@ "nyc": "^13.1.0", | ||
"pem": "^1.12.5", | ||
"resolve-alpn": "^1.0.0", | ||
"tempy": "^0.2.1", | ||
"timekeeper": "^2.2.0", | ||
"to-readable-stream": "^1.0.0", | ||
"xo": "^0.23.0" | ||
}, | ||
"ava": { | ||
"timeout": "2m" | ||
} | ||
} |
215
README.md
# http2-wrapper | ||
> HTTP2 client, but with the HTTP1 API | ||
> HTTP2 client, just with the familiar `https` API | ||
@@ -9,3 +9,5 @@ [![Build Status](https://travis-ci.org/szmarczak/http2-wrapper.svg?branch=master)](https://travis-ci.org/szmarczak/http2-wrapper) | ||
This package was created for the purpose of supporting HTTP2 without the need to rewrite your code.<br> | ||
**This package is under heavy development. It may contain bugs. Don't forget to report them if you notice any.** | ||
This package was created to support HTTP2 without the need to rewrite your code.<br> | ||
I recommend adapting to the [`http2`](https://nodejs.org/api/http2.html) module if possible - it's much simpler to use and has many cool features! | ||
@@ -15,2 +17,7 @@ | ||
## Installation | ||
> `$ npm install http2-wrapper`<br> | ||
> `$ yarn add http2-wrapper` | ||
## Usage | ||
@@ -31,11 +38,11 @@ ```js | ||
const req = http2.request(options, res => { | ||
console.log('statusCode:', res.statusCode); | ||
console.log('headers:', res.headers); | ||
const request = http2.request(options, response => { | ||
console.log('statusCode:', response.statusCode); | ||
console.log('headers:', response.headers); | ||
const body = []; | ||
res.on('data', chunk => { | ||
response.on('data', chunk => { | ||
body.push(chunk); | ||
}); | ||
res.on('end', () => { | ||
response.on('end', () => { | ||
console.log('body:', Buffer.concat(body).toString()); | ||
@@ -45,6 +52,6 @@ }); | ||
req.on('error', e => console.error(e)); | ||
request.on('error', e => console.error(e)); | ||
req.write('123'); | ||
req.end('456'); | ||
request.write('123'); | ||
request.end('456'); | ||
@@ -83,10 +90,11 @@ // statusCode: 200 | ||
### http2.auto(url, options) | ||
**Note**: the `session` option accepts an instance of [`Http2Session`](https://nodejs.org/api/http2.html#http2_class_http2session). To pass a SSL session, use `socketSession` instead. | ||
**Note**: [`resolve-alpn`](https://github.com/szmarczak/resolve-alpn) package required. | ||
### http2.auto(url, options, callback) | ||
Performs [ALPN](https://nodejs.org/api/tls.html#tls_alpn_and_sni) negotiation. | ||
Returns a Promise giving [`HTTP2ClientRequest`](https://github.com/szmarczak/http2-wrapper#http2http2clientrequest) instance or [`ClientRequest`](https://nodejs.org/api/https.html#https_class_https_clientrequest) instance (depending on the ALPN).<br> | ||
Usage example: | ||
Returns a Promise giving proper `ClientRequest` instance (depending on the ALPN). | ||
**Note**: the `agent` option also accepts an object with `http`, `https` and `http2` properties. | ||
```js | ||
@@ -108,10 +116,9 @@ 'use strict'; | ||
try { | ||
const req = await http2.auto(options); | ||
req.on('response', res => { | ||
console.log('statusCode:', res.statusCode); | ||
console.log('headers:', res.headers); | ||
const request = await http2.auto(options, response => { | ||
console.log('statusCode:', response.statusCode); | ||
console.log('headers:', response.headers); | ||
const body = []; | ||
res.on('data', chunk => body.push(chunk)); | ||
res.on('end', () => { | ||
response.on('data', chunk => body.push(chunk)); | ||
response.on('end', () => { | ||
console.log('body:', Buffer.concat(body).toString()); | ||
@@ -121,6 +128,6 @@ }); | ||
req.on('error', console.error); | ||
request.on('error', console.error); | ||
req.write('123'); | ||
req.end('456'); | ||
request.write('123'); | ||
request.end('456'); | ||
} catch (error) { | ||
@@ -156,74 +163,152 @@ console.error(error); | ||
#### url | ||
### http2.auto.resolveALPN(options) | ||
Type: `string` `URL` | ||
Resolves ALPN using HTTP options. | ||
### http2.request(url, options, callback) | ||
Same as [`https.request`](https://nodejs.org/api/https.html#https_https_request_options_callback). | ||
### http2.get(url, options, callback) | ||
Same as [`https.get`](https://nodejs.org/api/https.html#https_https_get_options_callback). | ||
### new http2.ClientRequest(url, options, callback) | ||
Same as [`https.ClientRequest`](https://nodejs.org/api/https.html#https_class_https_clientrequest). | ||
### new http2.IncomingMessage(socket) | ||
Same as [`https.IncomingMessage`](https://nodejs.org/api/https.html#https_class_https_incomingmessage). | ||
### new http2.Agent(options) | ||
**Note**: this is **not** compatible with the classic `http.Agent`. | ||
Usage example: | ||
```js | ||
'use strict'; | ||
const http2 = require('http2-wrapper'); | ||
class MyAgent extends http2.Agent { | ||
createConnection(authority, options) { | ||
console.log(`Connecting to ${authority}`); | ||
return http2.Agent.connect(authority, options); | ||
} | ||
} | ||
http2.get({ | ||
hostname: 'google.com', | ||
agent: new MyAgent() | ||
}, res => { | ||
res.on('data', chunk => console.log(`Received chunk of ${chunk.length} bytes`)); | ||
}); | ||
``` | ||
#### options | ||
Type: `object` | ||
Each option is assigned to each `Agent` instance and can be changed later. | ||
#### callback(error, clientRequestInstance) | ||
##### timeout | ||
Type: `Function` | ||
Type: `number`<br> | ||
Default: `30000` | ||
### http2.auto.resolveALPN(options) | ||
If there's no activity in given time (milliseconds), the session is closed. | ||
Resolves ALPN using HTTP options. | ||
##### maxSessions | ||
### http2.request(url, options, callback) | ||
Type: `number`<br> | ||
Default: `Infinity` | ||
Same as [https.request](https://nodejs.org/api/https.html#https_https_request_options_callback). | ||
Max sessions per origin. | ||
**Note**: `session` accepts [HTTP2 sessions](https://nodejs.org/api/http2.html#http2_http2_connect_authority_options_listener). To pass TLS session, you need to use `socketSession` instead. | ||
##### maxFreeSessions | ||
### http2.get(url, options, callback) | ||
Type: `number`<br> | ||
Default: `1` | ||
Same as [https.get](https://nodejs.org/api/https.html#https_https_get_options_callback). | ||
Max free sessions per origin. | ||
### http2.HTTP2ClientRequest | ||
#### agent.getName(authority, options) | ||
Same as [https.ClientRequest](https://nodejs.org/api/https.html#https_class_https_clientrequest). | ||
Returns a `string` containing a proper name for sessions created with these options. | ||
### http2.HTTP2IncomingMessage | ||
#### agent.getSession(authority, options) | ||
Same as [https.IncomingMessage](https://nodejs.org/api/https.html#https_class_https_incomingmessage). | ||
Returns a Promise giving free `Http2Session`. If no free sessions are found, a new one is created.<br> | ||
You can cancel the session query by calling `.cancel()` on the promise. | ||
## Tips | ||
##### authority | ||
### Reusing sessions | ||
Type: `string` | ||
Due to the lack of HTTP2 session pools, you have to manage them by yourself. To reuse a session, you need to pass it through the `session` option: | ||
Authority used to create a new session. | ||
```js | ||
const http2 = require('http2-wrapper'); | ||
const session = http2.connect('https://google.com'); | ||
session.unref(); // The session will destroy automatically when the process exits | ||
##### options | ||
const options = { | ||
hostname: 'google.com', | ||
session | ||
}; | ||
Type: `Object` | ||
// Reuses the same session | ||
const makeRequest = () => { | ||
http2.request(options, res => { | ||
res.on('data', chunk => { | ||
console.log(`Received ${chunk.length} bytes of data`); | ||
}); | ||
}).end(); | ||
}; | ||
Options used to create a new session. | ||
makeRequest(); // Received 220 bytes of data | ||
makeRequest(); // Received 220 bytes of data | ||
``` | ||
#### agent.request(authority, options, headers) | ||
Returns a Promise giving `Http2Stream`.<br> | ||
You can cancel the request by calling `.cancel()` on the promise, as well as using `.abort()` on the `ClientRequest` instance. | ||
#### agent.createConnection(authority, options) | ||
Returns a new TLS `Socket`. It defaults to `Agent.connect(authority, options)`. | ||
#### agent.closeFreeSessions() | ||
Makes an attempt to close free sessions. Only sessions with no concurrent streams are closed. | ||
#### agent.destroy() | ||
Destroys **all** sessions. | ||
## Notes | ||
- [WebSockets over HTTP2 is not supported](https://github.com/nodejs/node/issues/15230). | ||
- [HTTP2 sockets cannot be malformed](https://github.com/nodejs/node/blob/cc8250fab86486632fdeb63892be735d7628cd13/lib/internal/http2/core.js#L725). | ||
- HTTP2IncomingMessage has no [`close` event](https://nodejs.org/api/http.html#http_event_close_2) to prevent [memory leaks](https://github.com/szmarczak/http2-wrapper/blob/945f8b140541d2e8f0dfcb946a2ead3b4d3233ae/source/client-request.js#L72-L74). | ||
- There's no `pool` (`agent` equivalent) option [yet](https://github.com/nodejs/node/issues/17746). | ||
- [WebSockets over HTTP2 is not supported yet](https://github.com/nodejs/node/issues/15230), although there is [a proposal](https://tools.ietf.org/html/rfc8441) already. | ||
- [HTTP2 sockets cannot be malformed](https://github.com/nodejs/node/blob/cc8250fab86486632fdeb63892be735d7628cd13/lib/internal/http2/core.js#L725), therefore modifying the socket will have no effect. | ||
## Benchmarks | ||
CPU: Intel i7-7700k<br> | ||
Server: H2O 2.2.5 [`h2o.conf`](h2o.conf)<br> | ||
Node: v12.6.0<br> | ||
Commit: latest<br> | ||
``` | ||
http2-wrapper x 9,453 ops/sec ±6.98% (75 runs sampled) | ||
http2-wrapper - preconfigured session x 11,955 ops/sec ±1.89% (84 runs sampled) | ||
http2 x 18,478 ops/sec ±1.36% (84 runs sampled) | ||
http2 - using PassThrough x 16,099 ops/sec ±1.60% (84 runs sampled) | ||
https x 1,547 ops/sec ±3.93% (74 runs sampled) | ||
http x 6,138 ops/sec ±5.15% (74 runs sampled) | ||
Fastest is http2 | ||
``` | ||
`http2-wrapper`: | ||
- It's `1.95x` slower than `http2`. | ||
- It's `1.70x` slower than `http2` with `PassThrough`. | ||
- It's `6.11x` faster than `https`. | ||
- It's `1.54x` faster than `http`. | ||
`http2-wrapper - preconfigured session`: | ||
- It's `1.55x` slower than `http2`. | ||
- It's `1.35x` slower than `http2` with `PassThrough`. | ||
- It's `7.73x` faster than `https`. | ||
- It's `1.95x` faster than `http`. | ||
## Related | ||
- [`got`](https://github.com/sindresorhus/got) - Simplified HTTP requests | ||
## License | ||
MIT |
'use strict'; | ||
const http = require('http'); | ||
const https = require('https'); | ||
const Http2ClientRequest = require('./client-request'); | ||
const httpResolveALPN = require('./utils/http-resolve-alpn'); | ||
const urlToOptions = require('./utils/url-to-options'); | ||
const HTTP2ClientRequest = require('./client-request'); | ||
module.exports = async (input, options) => { | ||
const cache = {}; | ||
module.exports = async (input, options, callback) => { | ||
if (typeof input === 'string' || input instanceof URL) { | ||
@@ -13,27 +15,49 @@ input = urlToOptions(new URL(input)); | ||
options = {...input, ...options}; | ||
options.protocol = options.protocol || 'https:'; | ||
options.port = options.port || (options.protocol === 'http:' ? 80 : 443); | ||
options = { | ||
protocol: 'https:', | ||
...input, | ||
...options | ||
}; | ||
if (options.protocol === 'https:') { | ||
const {alpnProtocol, socket} = await httpResolveALPN({ | ||
...options, | ||
resolveSocket: !options.createConnection | ||
}); | ||
const host = options.hostname || options.host || 'localhost'; | ||
const port = options.port || 443; | ||
const ALPNProtocols = options.ALPNProtocols || ['h2', 'http/1.1']; | ||
const name = `${host}:${port}:${ALPNProtocols.sort()}`; | ||
if (socket) { | ||
options.createConnection = () => socket; | ||
let alpnProtocol = cache[name]; | ||
if (typeof alpnProtocol === 'undefined') { | ||
alpnProtocol = (await httpResolveALPN(options)).alpnProtocol; | ||
cache[name] = alpnProtocol; | ||
const keys = Object.keys(cache); | ||
/* istanbul ignore next */ | ||
if (keys.length > 100) { | ||
delete cache[keys.pop()]; | ||
} | ||
} | ||
if (alpnProtocol === 'h2') { | ||
return new HTTP2ClientRequest(options); | ||
if (options.agent && options.agent.http2) { | ||
options.agent = options.agent.http2; | ||
} | ||
return new Http2ClientRequest(options, callback); | ||
} | ||
if (options.agent && options.agent.https) { | ||
options.agent = options.agent.https; | ||
} | ||
options._defaultAgent = https.globalAgent; | ||
} else if (options.agent && options.agent.http) { | ||
options.agent = options.agent.http; | ||
} | ||
options.session = options.socketSession; | ||
return new http.ClientRequest(options); | ||
return new http.ClientRequest(options, callback); | ||
}; | ||
module.exports.resolveALPN = httpResolveALPN; | ||
module.exports.protocolCache = cache; |
'use strict'; | ||
const http2 = require('http2'); | ||
const {Writable} = require('stream'); | ||
const {Agent, globalAgent} = require('./agent'); | ||
const HTTP2IncomingMessage = require('./incoming-message'); | ||
@@ -8,10 +9,9 @@ const urlToOptions = require('./utils/url-to-options'); | ||
const { | ||
ERR_HTTP_HEADERS_SENT, | ||
ERR_INVALID_HTTP_TOKEN, | ||
ERR_HTTP_INVALID_HEADER_VALUE, | ||
ERR_INVALID_ARG_TYPE, | ||
ERR_INVALID_PROTOCOL | ||
ERR_INVALID_PROTOCOL, | ||
ERR_HTTP_HEADERS_SENT | ||
} = require('./utils/errors'); | ||
const { | ||
NGHTTP2_CANCEL, | ||
HTTP2_HEADER_STATUS, | ||
@@ -24,8 +24,10 @@ HTTP2_HEADER_METHOD, | ||
const kHeaders = Symbol('headers'); | ||
const kCustomSession = Symbol('customSession'); | ||
const kErrorHandler = Symbol('errorHandler'); | ||
const kAuthority = Symbol('authority'); | ||
const kAgent = Symbol('agent'); | ||
const kSession = Symbol('session'); | ||
const kOptions = Symbol('options'); | ||
const kFlushedHeaders = Symbol('flushedHeaders'); | ||
const kPendingPromise = Symbol('pendingPromise'); | ||
const noop = () => {}; | ||
class HTTP2ClientRequest extends Writable { | ||
class ClientRequest extends Writable { | ||
constructor(input, options, cb) { | ||
@@ -35,3 +37,2 @@ super(); | ||
if (typeof input === 'string' || input instanceof URL) { | ||
// (input, options, cb) | ||
input = urlToOptions(new URL(input)); | ||
@@ -45,8 +46,16 @@ } | ||
} else { | ||
// (input, options, cb) | ||
options = {...input, ...options}; | ||
} | ||
options.protocol = options.protocol || 'https:'; | ||
options.port = options.port || 443; | ||
const defaultPort = options.defaultPort || (this.agent && this.agent.defaultPort); | ||
options = { | ||
protocol: 'https:', | ||
port: defaultPort || 443, | ||
preconnect: true, | ||
...options, | ||
host: options.hostname || options.host || 'localhost' | ||
}; | ||
if (options.protocol !== 'https:') { | ||
@@ -56,63 +65,61 @@ throw new ERR_INVALID_PROTOCOL(options.protocol, 'https:'); | ||
const {timeout} = options; | ||
delete options.timeout; | ||
this[kHeaders] = Object.create(null); | ||
this[kErrorHandler] = error => this.emit('error', error); | ||
this.socket = null; | ||
this.connection = null; | ||
// Don't confuse with the `net` module. | ||
this.method = options.method ? options.method.toUpperCase() : 'GET'; | ||
this.path = options.path || '/'; | ||
this.session = options.session; | ||
options.path = options.socketPath; | ||
options.session = options.socketSession; | ||
if (this.session) { | ||
this[kCustomSession] = true; | ||
} else { | ||
this[kCustomSession] = false; | ||
this.session = http2.connect(options.authority || options, options); | ||
this.res = null; | ||
this.aborted = false; | ||
if (options.headers) { | ||
for (const [header, value] of Object.entries(options.headers)) { | ||
this[kHeaders][header.toLowerCase()] = value; | ||
} | ||
} | ||
// Forwards errors to this instance. | ||
this.session.once('error', this[kErrorHandler]); | ||
if (options.auth && !Reflect.has(this[kHeaders], 'authorization')) { | ||
this[kHeaders].authorization = 'Basic ' + Buffer.from(options.auth).toString('base64'); | ||
} | ||
// Note: response `close` event is not supported, | ||
// because if you keep the session open till the process exits | ||
// there will be a memory leak. | ||
if (options.agent) { | ||
this[kAgent] = options.agent; | ||
} else if (options.session) { | ||
this[kSession] = options.session; | ||
} else if (options.agent === false) { | ||
this[kAgent] = new Agent({maxFreeSessions: 0}); | ||
} else if (options.agent === null || typeof options.agent === 'undefined') { | ||
if (typeof options.createConnection !== 'function') { | ||
this[kAgent] = globalAgent; | ||
} | ||
} else if (typeof options.agent.request !== 'function') { | ||
throw new ERR_INVALID_ARG_TYPE('options.agent', ['Agent-like Object', 'undefined', 'false'], options.agent); | ||
} | ||
this.socket = this.session.socket; | ||
this.connection = this.socket; | ||
options.ALPNProtocols = ['h2']; | ||
options.session = options.socketSession; | ||
options.path = options.socketPath; | ||
this.method = options.method || 'GET'; | ||
this[kOptions] = options; | ||
this[kAuthority] = options.authority || `https://${options.hostname || options.host}:${options.port}`; | ||
if (cb) { | ||
this.once('response', cb); | ||
if (this[kAgent] && options.preconnect) { | ||
this[kPendingPromise] = this[kAgent].getSession(this[kAuthority], options); | ||
this[kPendingPromise].catch(() => {}); | ||
} | ||
this.res = null; | ||
this.aborted = false; | ||
try { | ||
if (options.headers) { | ||
for (const [header, value] of Object.entries(options.headers)) { | ||
this.setHeader(header, value); | ||
} | ||
} | ||
if (options.auth && !Reflect.has(this[kHeaders], 'authorization')) { | ||
this.setHeader('authorization', 'Basic ' + Buffer.from(options.auth).toString('base64')); | ||
} | ||
} catch (error) { | ||
if (!this[kCustomSession]) { | ||
this.session.close(); | ||
} | ||
throw error; | ||
if (timeout) { | ||
this.setTimeout(timeout); | ||
} | ||
if (options.timeout) { | ||
this.setTimeout(options.timeout); | ||
if (cb) { | ||
this.once('response', cb); | ||
} | ||
// Move the `socket` event to next tick, so the user can | ||
// attach the listener before the event has been emitted. | ||
process.nextTick(() => this.emit('socket', this.socket)); | ||
this[kFlushedHeaders] = false; | ||
} | ||
@@ -122,12 +129,20 @@ | ||
this.flushHeaders(); | ||
this._request.write(chunk, encoding, callback); | ||
const callWrite = () => this._request.write(chunk, encoding, callback); | ||
if (this._request) { | ||
callWrite(); | ||
} else { | ||
this.once('socket', callWrite); | ||
} | ||
} | ||
_final(callback) { | ||
if (this.session.destroyed) { | ||
return; | ||
this.flushHeaders(); | ||
const callEnd = () => this._request.end(callback); | ||
if (this._request) { | ||
callEnd(); | ||
} else { | ||
this.once('socket', callEnd); | ||
} | ||
this.flushHeaders(); | ||
this._request.end(callback); | ||
} | ||
@@ -137,3 +152,3 @@ | ||
if (!this.aborted) { | ||
process.nextTick(self => self.emit('abort'), this); | ||
process.nextTick(() => this.emit('abort')); | ||
} | ||
@@ -147,5 +162,5 @@ this.aborted = true; | ||
if (this._request) { | ||
this._request.close(); | ||
} else if (!this[kCustomSession]) { | ||
this.session.close(); | ||
this._request.close(NGHTTP2_CANCEL); | ||
} else if (this[kPendingPromise]) { | ||
this[kPendingPromise].cancel(); | ||
} | ||
@@ -157,10 +172,4 @@ } | ||
this._request.destroy(error); | ||
} else { | ||
if (!this[kCustomSession]) { | ||
this.session.close(); | ||
} | ||
if (error) { | ||
process.nextTick(() => this.emit('error', error)); | ||
} | ||
} else if (error) { | ||
process.nextTick(() => this.emit('error', error)); | ||
} | ||
@@ -170,89 +179,105 @@ } | ||
flushHeaders() { | ||
if (!this._request && !this.destroyed) { | ||
// Makes the real request. | ||
const headers = { | ||
[HTTP2_HEADER_METHOD]: this.method, | ||
...this[kHeaders] | ||
}; | ||
if (this[kFlushedHeaders]) { | ||
return; | ||
} | ||
// Update the `method` property in case user changed it. | ||
this.method = headers[HTTP2_HEADER_METHOD].toUpperCase(); | ||
const isConnectMethod = this.method === HTTP2_METHOD_CONNECT; | ||
this[kFlushedHeaders] = true; | ||
// Must not specify the `:path` and `:scheme` headers for `CONNECT` requests. | ||
if (!isConnectMethod) { | ||
headers[HTTP2_HEADER_PATH] = this.path; | ||
} | ||
const isConnectMethod = this.method === HTTP2_METHOD_CONNECT; | ||
this._request = this.session.request(headers, { | ||
endStream: false | ||
}); | ||
const headers = { | ||
...this[kHeaders], | ||
[HTTP2_HEADER_METHOD]: this.method | ||
}; | ||
// Forwards `timeout`, `continue`, `close` and `error` events to this instance. | ||
if (!isConnectMethod) { | ||
proxyEvents(this._request, this, ['timeout', 'continue', 'close']); | ||
this._request.once('error', error => this.emit('error', Reflect.has(error, 'cause') ? error.cause : error)); | ||
} | ||
// Must not specify the `:path` and `:scheme` headers for `CONNECT` requests. | ||
if (!isConnectMethod) { | ||
headers[HTTP2_HEADER_PATH] = this.path; | ||
} | ||
// Detaches error handling (the `http2` module will pass them to the request instance). | ||
this.session.removeListener('error', this[kErrorHandler]); | ||
this.session.once('error', noop); | ||
// The real magic is here | ||
const onStream = stream => { | ||
this._request = stream; | ||
// Clean up error handling. We're done. | ||
this._request.once('end', () => this.session.removeListener('error', noop)); | ||
if (!this.destroyed && !this.aborted) { | ||
// Forwards `timeout`, `continue`, `close` and `error` events to this instance. | ||
if (!isConnectMethod) { | ||
proxyEvents(this._request, this, ['timeout', 'continue', 'close', 'error']); | ||
} | ||
// This event tells we are ready to listen for the data. | ||
this._request.once('response', (headers, flags, rawHeaders) => { | ||
this.res = new HTTP2IncomingMessage(this.socket); | ||
this.res.req = this; | ||
this.res.statusCode = headers[HTTP2_HEADER_STATUS]; | ||
this.res.headers = {...headers}; | ||
this.res.rawHeaders = [...rawHeaders]; | ||
// This event tells we are ready to listen for the data. | ||
this._request.once('response', (headers, flags, rawHeaders) => { | ||
this.res = new HTTP2IncomingMessage(this.socket); | ||
this.res.req = this; | ||
this.res.statusCode = headers[HTTP2_HEADER_STATUS]; | ||
this.res.headers = {...headers}; | ||
this.res.rawHeaders = [...rawHeaders]; | ||
this.res.once('end', () => { | ||
if (this.aborted) { | ||
this.res.aborted = true; | ||
this.res.emit('aborted'); | ||
} else { | ||
this.res.complete = true; | ||
} | ||
}); | ||
this.res.once('end', () => { | ||
if (this.aborted) { | ||
this.res.aborted = true; | ||
this.res.emit('aborted'); | ||
} else { | ||
this.res.complete = true; | ||
} | ||
}); | ||
if (isConnectMethod) { | ||
this.res.upgrade = true; | ||
if (isConnectMethod) { | ||
this.res.upgrade = true; | ||
// The HTTP1 API says the socket is detached here, | ||
// but we can't do that so we pass the original HTTP2 request. | ||
if (this.emit('connect', this.res, this._request, Buffer.alloc(0))) { | ||
this.emit('close'); | ||
// The HTTP1 API says the socket is detached here, | ||
// but we can't do that so we pass the original HTTP2 request. | ||
if (this.emit('connect', this.res, this._request, Buffer.alloc(0))) { | ||
this.emit('close'); | ||
} else { | ||
// No listeners attached, destroy the original request. | ||
this._request.destroy(); | ||
} | ||
} else { | ||
// No listeners attached, destroy the original request. | ||
this._request.destroy(); | ||
} | ||
} else { | ||
// Forwards data | ||
this._request.pipe(this.res); | ||
// Forwards data | ||
this._request.pipe(this.res); | ||
if (!this.emit('response', this.res)) { | ||
// No listeners attached, dump the response. | ||
this.res._dump(); | ||
if (!this.emit('response', this.res)) { | ||
// No listeners attached, dump the response. | ||
this.res._dump(); | ||
} | ||
} | ||
} | ||
}); | ||
}); | ||
// Emits `information` event | ||
this._request.once('headers', headers => this.emit('information', {statusCode: headers[HTTP2_HEADER_STATUS]})); | ||
// Emits `information` event | ||
this._request.once('headers', headers => this.emit('information', {statusCode: headers[HTTP2_HEADER_STATUS]})); | ||
this._request.once('trailers', (trailers, flags, rawTrailers) => { | ||
// Assigns trailers to the response object. | ||
this.res.trailers = {...trailers}; | ||
this.res.rawTrailers = [...rawTrailers]; | ||
}); | ||
this._request.once('trailers', (trailers, flags, rawTrailers) => { | ||
// Assigns trailers to the response object. | ||
this.res.trailers = {...trailers}; | ||
this.res.rawTrailers = [...rawTrailers]; | ||
}); | ||
if (!this[kCustomSession]) { | ||
// The session is ours, we won't reuse it. | ||
this._request.once('close', () => this.session.close()); | ||
this.socket = this._request.session.socket; | ||
this.connection = this._request.session.socket; | ||
process.nextTick(() => { | ||
this.emit('socket', this._request.session.socket); | ||
}); | ||
} else { | ||
this._request.close(NGHTTP2_CANCEL); | ||
} | ||
}; | ||
this.emit('flushedHeaders'); | ||
// Makes a HTTP2 request | ||
if (this[kSession]) { | ||
try { | ||
onStream(this[kSession].request(headers, { | ||
endStream: false | ||
})); | ||
} catch (error) { | ||
this.emit('error', error); | ||
} | ||
} else { | ||
this[kPendingPromise] = this[kAgent].request(this[kAuthority], this[kOptions], headers); | ||
// eslint-disable-next-line promise/prefer-await-to-then | ||
this[kPendingPromise].then(onStream, error => { | ||
this.emit('error', error); | ||
}); | ||
} | ||
@@ -262,6 +287,2 @@ } | ||
getHeader(name) { | ||
if (typeof name !== 'string') { | ||
throw new ERR_INVALID_ARG_TYPE('name', 'string', name); | ||
} | ||
return this[kHeaders][name.toLowerCase()]; | ||
@@ -271,7 +292,3 @@ } | ||
get headersSent() { | ||
if (this._request) { | ||
return Boolean(this._request.sentHeaders); | ||
} | ||
return false; | ||
return this[kFlushedHeaders]; | ||
} | ||
@@ -283,5 +300,2 @@ | ||
} | ||
if (name === '' || typeof name !== 'string') { | ||
throw new ERR_INVALID_HTTP_TOKEN('Header name', name); | ||
} | ||
@@ -295,8 +309,2 @@ delete this[kHeaders][name.toLowerCase()]; | ||
} | ||
if (name === '' || typeof name !== 'string') { | ||
throw new ERR_INVALID_HTTP_TOKEN('Header name', name); | ||
} | ||
if (value === undefined) { | ||
throw new ERR_HTTP_INVALID_HEADER_VALUE(value, name); | ||
} | ||
@@ -318,3 +326,3 @@ this[kHeaders][name.toLowerCase()] = value; | ||
} else { | ||
this.once('flushedHeaders', () => { | ||
this.once('socket', () => { | ||
this._request.setTimeout(ms, cb); | ||
@@ -326,4 +334,16 @@ }); | ||
} | ||
get maxHeadersCount() { | ||
if (this._request) { | ||
return this._request.session.localSettings.maxHeaderListSize; | ||
} | ||
return undefined; | ||
} | ||
set maxHeadersCount(_value) { | ||
// Updating HTTP2 settings would affect all requests, do nothing. | ||
} | ||
} | ||
module.exports = HTTP2ClientRequest; | ||
module.exports = ClientRequest; |
'use strict'; | ||
const {PassThrough} = require('stream'); | ||
class HTTP2IncomingMessage extends PassThrough { | ||
class IncomingMessage extends PassThrough { | ||
constructor(socket) { | ||
@@ -49,2 +49,2 @@ super(); | ||
module.exports = HTTP2IncomingMessage; | ||
module.exports = IncomingMessage; |
'use strict'; | ||
const http2 = require('http2'); | ||
const HTTP2ClientRequest = require('./client-request'); | ||
const HTTP2IncomingMessage = require('./incoming-message'); | ||
const agent = require('./agent'); | ||
const ClientRequest = require('./client-request'); | ||
const IncomingMessage = require('./incoming-message'); | ||
const auto = require('./auto'); | ||
const request = (url, options, cb) => { | ||
return new HTTP2ClientRequest(url, options, cb); | ||
return new ClientRequest(url, options, cb); | ||
}; | ||
@@ -20,7 +21,8 @@ | ||
...http2, | ||
...agent, | ||
auto, | ||
request, | ||
get, | ||
HTTP2ClientRequest, | ||
HTTP2IncomingMessage | ||
ClientRequest, | ||
IncomingMessage | ||
}; |
'use strict'; | ||
/* istanbul ignore file: https://github.com/nodejs/node/blob/master/lib/internal/errors.js */ | ||
module.exports = {}; | ||
const makeError = (Base, key, getMessage) => { | ||
@@ -10,44 +8,8 @@ module.exports[key] = class NodeError extends Base { | ||
super(typeof getMessage === 'string' ? getMessage : getMessage(args)); | ||
this.name = `${super.name} [${key}]`; | ||
this.code = key; | ||
} | ||
get name() { | ||
return `${super.name} [${key}]`; | ||
} | ||
set name(value) { | ||
Object.defineProperty(this, 'name', { | ||
configurable: true, | ||
enumerable: true, | ||
value, | ||
writable: true | ||
}); | ||
} | ||
get code() { | ||
return key; | ||
} | ||
set code(value) { | ||
Object.defineProperty(this, 'code', { | ||
configurable: true, | ||
enumerable: true, | ||
value, | ||
writable: true | ||
}); | ||
} | ||
}; | ||
}; | ||
makeError(Error, 'ERR_HTTP_HEADERS_SENT', args => { | ||
return `Cannot ${args[0]} headers after they are sent to the client`; | ||
}); | ||
makeError(TypeError, 'ERR_INVALID_HTTP_TOKEN', args => { | ||
return `${args[0]} must be a valid HTTP token ["${args[1]}"]`; | ||
}); | ||
makeError(TypeError, 'ERR_HTTP_INVALID_HEADER_VALUE', args => { | ||
return `Invalid value "${args[0]}" for header "${args[1]}"`; | ||
}); | ||
makeError(TypeError, 'ERR_INVALID_ARG_TYPE', args => { | ||
@@ -61,1 +23,5 @@ const type = args[0].includes('.') ? 'property' : 'argument'; | ||
}); | ||
makeError(Error, 'ERR_HTTP_HEADERS_SENT', args => { | ||
return `Cannot ${args[0]} headers after they are sent to the client`; | ||
}); |
@@ -11,3 +11,3 @@ 'use strict'; | ||
port: options.port || 443, | ||
host: options.hostname || options.host, | ||
host: options.hostname || options.host || 'localhost', | ||
path: options.socketPath, | ||
@@ -14,0 +14,0 @@ session: options.socketSession, |
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
Network access
Supply chain riskThis module accesses the network.
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
30831
13
696
307
1
13
7
+ Addedresolve-alpn@^1.0.0
+ Addedresolve-alpn@1.2.1(transitive)