Initial commit

This commit is contained in:
2025-03-07 19:22:02 +01:00
commit 4a98255d83
55743 changed files with 5280367 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
language: node_js
node_js:
- 8.11.2
- 10.3.0
cache:
directories:
- node_modules
install:
- npm install
script:
- npm run test
after_script: npm install coveralls@3.0.1 && npm run test-ci && cat ./coverage/lcov.info
| coveralls
deploy:
provider: npm
email: hisco@googlegroups.com
api_key:
secure: OaMiQbh8vZLWjQ3uoALTwSFiWO0lOKFZUR6m2+g00UwIgukXev9CF/WQGWeW3Z4Gj57fZyuyHTys7NsGNmfSvwPqm63HTLGTy8aUuG7xVEDdjcGepqPf19J0s0TGW1Y7vxzvwkBDmV3x9qBFGwmjRjsQ+vQ/jQ2XX5DlCy2NqSyTwcGDsMM09worRAmfc/Q5PGn+sngl0LQFd0vSVWAI9M5kztUTxl2xQFH80Nt6j11r8R1+aqkHhHuJLuYHZGAV9Xf6VjrJ3e2b29SeeTmii1EnphJsljHzjoEpluuGwfQIdOEpbSfXqVxQR1RyFfVR/KE0kJppKLucykPz4PA+e5Ak9hTFt5D44/wHDsDNPpGAlLCdKSzkPciPk3gPHR310m0B5Mb01fAEkCZ9AEQuc/Q7rJtthE9eriK7741Zin5rtxfInMyrwTfVpKyuwBoqlMZjBzuo9EfnaUXZ0VrccX9cu0yz4qXGzziYoMmBDZ/wYaXnzWT9d6Q95nFxetC9l7KoLBUQM/TtEhmiPTJcd3hj75/cuAA6/XWTs6yQ7il5DgstvnHPxbpCnH/eedFTubRv5amSmI97r3als0B4m7bCWFG3M8ZJB1WovZ+Rymzw0Fqb9nt4eeiJE1m1qqZLUn0/eo4T0wbAUyPx2Zq0RKFvjqKfRaQXDE/TeJSGOfc=
on:
tags: true
repo: hisco/http2-client
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Eyal.D <hisco@googlegroups.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+184
View File
@@ -0,0 +1,184 @@
# HTTP2 client
[![Greenkeeper badge](https://badges.greenkeeper.io/hisco/http2-client.svg)](https://greenkeeper.io/)
[![NPM Version][npm-image]][npm-url]
[![Build Status][travis-image]][travis-url]
[![Known Vulnerabilities][snyk-image]][snyk-url]
Drop-in replacement for Nodes http and https that transparently make http request to both http1 / http2 server.
Currently, it's the only http2/https compatible API for clients.
## Motivation
http2 in Node.JS works entirely differently, while in browsers the experience is the same.
`http2-client` was created to enable http2 / http1.1 requests with the same interface as http1.1.
The reason is that many NPM modules cannot upgrade to use http2.0 as these are coupled into http1.1 interface.
With `http2-client` it should be very straight forward.
Meaning you don't need to know which protocol the destination supports before making the request `http2-client` will chose the one that works.
If the Node.js version you are using is not supporting http2 `http2-client` will automatically fallback to http.
## Features
Transparently supports all http protocol.
* Http/1.1
* Https/1.1
* Http/2.0
In case of http1.1
* Connection pool is managed as usual with an http agent.
In case of http2.0
* Connection pool is managed by Http2 agent.
* Requests to the same "origin" will use the same tcp connection (per request manager) - automatically.
* All Http2 features are available except push.
## Usage - Same interface
### request()
```js
const {request} = require('http2-client');
const h1Target = 'http://www.example.com/';
const h2Target = 'https://www.example.com/';
const req1 = request(h1Target, (res)=>{
console.log(`
Url : ${h1Target}
Status : ${res.statusCode}
HttpVersion : ${res.httpVersion}
`);
});
req1.end();
const req2 = request(h2Target, (res)=>{
console.log(`
Url : ${h2Target}
Status : ${res.statusCode}
HttpVersion : ${res.httpVersion}
`);
});
req2.end();
```
### get()
```js
const {get} = require('http2-client');
const h1Target = 'http://www.example.com/';
const h2Target = 'https://www.example.com/';
get(h1Target, (res)=>{
console.log(`
Url : ${h1Target}
Status : ${res.statusCode}
HttpVersion : ${res.httpVersion}
`);
});
get(h2Target, (res)=>{
console.log(`
Url : ${h2Target}
Status : ${res.statusCode}
HttpVersion : ${res.httpVersion}
`);
});
```
## API
The module mimics the nodejs http module interface of ClientRequest, get() and request().
Same API as regular http/s modules.
Different options will be used depending on the destination this method will get.
* Http/1.1
* Https/1.1
* Http/2.0
### HttpRequestManager
By default this module exports a default request method the will try to detect the currect protocol to use (http2/http1.1/https1.1).
However, you can always create different request manager with your specfic defaults and seperated cache.
* options `<Object>`
* keepH2ConnectionFor `<number>` Time to keep http2 connection after used last time. Default: 1000ms.
* keepH1IdentificationCacheFor `<number>` TTL time for identification results of http1.1. Default: 30000ms.
* useHttp `<boolean>` Should enforce http socket.
* useHttps `<boolean>` Should enforce https socket.
```js
//Use the default
const {request} = require('http2-client');
//Make a request
const req = request(/*....*/);
req.end();
//Alternatively create a new request
const {HttpRequestManager} = require('http2-client');
const httpRequestManager = new HttpRequestManager();
//Make a request
const req = httpRequestManager.request(/*....*/);
req.end();
```
### Http/1.1 - request(options[, callback]) | request(url [,options][, callback])
* options `<Object> | <string> | <URL>`
* protocol `<string>` Protocol to use. Default: 'http:'.
* host `<string>` A domain name or IP address of the server to issue the request to. Default: 'localhost'.
* hostname `<string>` Alias for host. To support url.parse(), hostname is preferred over host.
* family `<number>` IP address family to use when resolving host and hostname. Valid values are 4 or 6.When unspecified, both IP v4 and v6 will be used.
* port `<number>` Port of remote server. Default: 80.
* localAddress `<string>` Local interface to bind for network connections.
* socketPath `<string>` Unix Domain Socket (use one of host:port or socketPath).
* method `<string>` A string specifying the HTTP request method. Default: 'GET'.
* path `<string>` Request path. Should include query string if any. E.G. '/index.html?page=12'. An exception is thrown when the request path contains illegal characters. Currently, only spaces are rejected but that may change in the future. Default: '/'.
* headers <Object> An object containing request headers.
* auth `<string>` Basic authentication i.e. 'user:password' to compute an Authorization header.
* agent `<http.Agent> | <boolean>` Controls Agent behavior. Possible values:
* undefined (default): use http.globalAgent for this host and port.
* Agent object: explicitly use the passed in Agent.
* false: causes a new Agent with default values to be used.
* createConnection <Function> A function that produces a socket/stream to use for the request when the agent option is not used. This can be used to avoid creating a custom Agent class just to override the default createConnection function. See agent.createConnection() for more details. Any Duplex stream is a valid return value.
* timeout `<number>` : A number specifying the socket timeout in milliseconds. This will set the timeout before the socket is connected.
* setHost `<boolean>`: Specifies whether or not to automatically add the Host header. Defaults to true.
* callback `<Function>`
* Returns: `<ClientRequest>`
### All http protocols - get(options[, callback]) | get(url [,options][, callback])
* Differences are per protocol as described in relevant request() and protocol.
* Same interface as request() with the method always set to GET. Properties that are inherited from the prototype are ignored.
* Since most requests are GET requests without bodies, Node.js provides this convenience method. The only difference between this method and http.request() is that it sets the method to GET and calls req.end() automatically
### Https/1.1 - request(options[, callback]) | request(url [,options][, callback])
* options `<Object> | <string> | <URL>` Accepts all options from Http/1.1 , with some differences in default values and aditional tls options:
* protocol Default: 'https:'
* port Default: 443
* agent Default: https.globalAgent
* rejectUnauthorized `<boolean>` If not false, the server certificate is verified against the list of supplied CAs. An 'error' event is emitted if verification fails; err.code contains the OpenSSL error code. Default: true.
* ALPNProtocols: `<string[]> | <Buffer[]> | <Uint8Array[]> | <Buffer> | <Uint8Array>` An array of strings, Buffers or Uint8Arrays, or a single Buffer or Uint8Array containing the supported ALPN protocols. Buffers should have the format [len][name][len][name]... e.g. 0x05hello0x05world, where the first byte is the length of the next protocol name. Passing an array is usually much simpler, e.g. ['hello', 'world'].
* servername: `<string>` Server name for the SNI (Server Name Indication) TLS extension.
* checkServerIdentity(servername, cert) <Function> A callback function to be used (instead of the builtin tls.checkServerIdentity() function) when checking the server's hostname (or the provided servername when explicitly set) against the certificate. This should return an <Error> if verification fails. The method should return undefined if the servername and cert are verified.
* session `<Buffer>` A Buffer instance, containing TLS session.
* minDHSize `<number>` Minimum size of the DH parameter in bits to accept a TLS connection. When a server offers a DH parameter with a size less than minDHSize, the TLS connection is destroyed and an error is thrown. Default: 1024.
* secureContext: Optional TLS context object created with tls.createSecureContext(). If a secureContext is not provided, one will be created by passing the entire options object to tls.createSecureContext().
* lookup: `<Function>` Custom lookup function. Default: dns.lookup().
* callback `<Function>`
* Returns: `<ClientRequest>`
### Https/2.0 - request(options[, callback]) | request(url [,options][, callback])
* options `<Object> | <string> | <URL>` Accepts all options from Https/1.1
* callback `<Function>`
* Returns: `<ClientRequest>`
## How?
`http2-client` implements 'Application-Layer Protocol Negotiation (ALPN)'.
Which means it first creates TCP connection, after successful ALPN negotiation the supported protocol is known.
If the supported protocol is http2.0 `http2-client` will re-use the same connection.
After the http2.0 connection won't be used for `keepH2ConnectionFor` which defaults to 100 ms, it will be automatically closed.
If the supported protocol is http1.x `http2-client` will only cache the identification result and not the actual socket for `keepH1IdentificationCacheFor` which defaults to 30000 ms.
Any socket configuration is manged by the http agent.
If none is defined the node `globalAgent` will be used.
## License
[MIT](LICENSE)
[npm-image]: https://img.shields.io/npm/v/http2-client.svg
[npm-url]: https://npmjs.org/package/http2-client
[travis-image]: https://img.shields.io/travis/hisco/http2-client/master.svg?style=flat-square
[travis-url]: https://travis-ci.org/hisco/http2-client
[snyk-image]: https://snyk.io/test/github/hisco/http2-client/badge.svg?targetFile=package.json
[snyk-url]: https://snyk.io/test/github/hisco/http2-client/badge.svg?targetFile=package.json
+20
View File
@@ -0,0 +1,20 @@
const {request} = require('../lib/index');
const h1Target = 'http://www.example.com/';
const h2Target = 'https://www.example.com/';
const req1 = request(h1Target, (res)=>{
console.log(`
Url : ${h1Target}
Status : ${res.statusCode}
HttpVersion : ${res.httpVersion}
`);
});
req1.end();
const req2 = request(h2Target, (res)=>{
console.log(`
Url : ${h2Target}
Status : ${res.statusCode}
HttpVersion : ${res.httpVersion}
`);
});
req2.end();
+11
View File
@@ -0,0 +1,11 @@
const http = require('http');
const h1Target = 'http://www.example.com/';
const req1 = http.request(h1Target, (res)=>{
console.log(`
Url : ${h1Target}
Status : ${res.statusCode}
HttpVersion : ${res.httpVersion}
`);
});
req1.end();
+1
View File
@@ -0,0 +1 @@
module.exports = require('./lib/http');
+1
View File
@@ -0,0 +1 @@
module.exports = require('./lib/https');
+17
View File
@@ -0,0 +1,17 @@
const {
HttpRequest,ClientRequest
} = require('./request');
const globalManager = HttpRequest.globalManager;
const request = globalManager.request.bind(globalManager);
const get = globalManager.get.bind(globalManager);
const http = Object.assign({},require('http'));
module.exports = Object.assign(http , {
ClientRequest,
globalManager,
request,
get
})
+18
View File
@@ -0,0 +1,18 @@
const {
HttpsRequest,
ClientRequest
} = require('./request');
const globalManager = HttpsRequest.globalManager;
const request = globalManager.request.bind(globalManager);
const get = globalManager.get.bind(globalManager);
const https = Object.assign({},require('https'));
module.exports = Object.assign(https , {
ClientRequest,
globalManager,
request,
get
})
+21
View File
@@ -0,0 +1,21 @@
const {
HttpRequestManager , HTTP2OutgoingMessage,ClientRequest
} = require('./request');
const http = require('./http');
const https = require('./https');
const autoDetectManager = new HttpRequestManager();
HttpRequestManager.globalManager = autoDetectManager;
const request = autoDetectManager.request.bind(autoDetectManager);
const get = autoDetectManager.get.bind(autoDetectManager);
module.exports = {
HTTP2OutgoingMessage,
ClientRequest,
globalManager : HttpRequestManager.globalManager ,
request,
get,
http: http,
https: https
}
+38
View File
@@ -0,0 +1,38 @@
const {assertIsObject } = require('./utils');
function initializeOptions(options) {
assertIsObject(options, 'options');
options = Object.assign({}, options);
options.allowHalfOpen = true;
options.rejectUnauthorized = false;
assertIsObject(options.settings, 'options.settings');
options.settings = Object.assign({}, options.settings);
// Used only with allowHTTP1
options.Http1IncomingMessage = options.Http1IncomingMessage ||
this.http.IncomingMessage;
options.Http1ServerResponse = options.Http1ServerResponse ||
this.http.ServerResponse;
options.Http2ServerRequest = options.Http2ServerRequest ||
((this.http2 || {}).Http2ServerRequest);
options.Http2ServerResponse = options.Http2ServerResponse ||
((this.http2 || {}).Http2ServerResponse);
return options;
}
function initializeTLSOptions(options, servername) {
options = initializeOptions.call(this,options);
var ALPNProtocols = options.ALPNProtocols = [];
if (this.http2Support)
ALPNProtocols.push('h2');
if (options.allowHTTP1 == true || !this.http2Support)
ALPNProtocols.push('http/1.1');
if (servername !== undefined && options.servername === undefined)
options.servername = servername;
return options;
}
module.exports = {
initializeTLSOptions
}
+586
View File
@@ -0,0 +1,586 @@
const { URL } = require('url');
const {EventEmitter} = require('events');
const _extend = require('util')._extend;
const {DebounceTimers , assertIsObject , ERR_INVALID_ARG_TYPE} = require('./utils');
const {initializeTLSOptions } = require('./request-options');
const http = require('http');
const https = require('https');
const {Stream} = require('stream');
function addFunctions(container , obj){
const proto = obj.prototype;
Object.keys(proto).forEach((name)=>{
if (container.indexOf(name)!=-1)
return;
if (name.indexOf('_')!=0 && typeof proto[name] == 'function'){
container.push(name);
}
})
}
const STUBBED_METHODS_NAME = [
]
//We need to proxy all v1 function
addFunctions(STUBBED_METHODS_NAME, http.ClientRequest);
addFunctions(STUBBED_METHODS_NAME, http.OutgoingMessage);
addFunctions(STUBBED_METHODS_NAME, EventEmitter);
addFunctions(STUBBED_METHODS_NAME, Stream);
const PROPERTIES_TO_PROXY = [
'httpVersionMajor',
'httpVersionMinor',
'httpVersion',
];
const HEADERS_TO_REMOVE = ['host' , 'connection']
const $stubs = Symbol('stubs');
function ClientRequest(){
this.http2Mimic = true;
this[$stubs] = [];
for (var i=0;i<STUBBED_METHODS_NAME.length;i++){
let name = STUBBED_METHODS_NAME[i];
if (!ClientRequest.prototype[name]){
this[name] = function method(){
return this.genericStubber(name , arguments);
}.bind(this);
}
}
var requestOptions , cb,url , args;
const isInternal = arguments[0] instanceof RequestInternalEnforce;
var isInternalMethod,isInternalProtocol;
if (isInternal){
const enforceOptions = arguments[0];
if ( enforceOptions.method)
isInternalMethod = enforceOptions.method;
if ( enforceOptions.protocol)
isInternalProtocol = enforceOptions.protocol;
}
if (isInternal){
args = arguments[0].args;
}
else{
args = arguments;
}
if (args[2] != undefined){
url = args[0];
requestOptions = args[1];
cb = args[2];
}
else if(args[1] == undefined){
requestOptions = args[0];
}
else{
requestOptions = args[0];
cb = args[1];
}
cb = cb || function dummy(){};
if (typeof requestOptions === 'string') {
requestOptions = urlToOptions(new URL(requestOptions));
if (!requestOptions.hostname) {
throw new Error('Unable to determine the domain name');
}
}
else {
if (url){
requestOptions = _extend(urlToOptions(new URL(url)), requestOptions);
}
else{
requestOptions = _extend({}, requestOptions);
}
}
if (isInternalProtocol!=isInternalProtocol){
requestOptions.protocol = isInternalProtocol;
}
if (requestOptions.protocol == 'https:' && !requestOptions.port && requestOptions.port !=0)
requestOptions.port = 443;
if (!requestOptions.port && requestOptions.port !=0)
requestOptions.port = 80;
if (isInternalMethod){
requestOptions.method = isInternalMethod;
}
else if (!requestOptions.method )
requestOptions.method = 'GET';
requestOptions.method = requestOptions.method.toUpperCase();
const requestManager = requestOptions.requestManager || this.getGlobalManager(requestOptions);
requestManager.handleClientRequest(this , requestOptions , cb);
}
ClientRequest.prototype = {
getGlobalManager(options){
if (options.agent)
return (options.agent.protocol == 'https:' ? HttpsRequest.globalManager : HttpRequest.globalManager);
else
return HttpRequestManager.globalManager;
},
genericStubber(method , args){
if (this[$stubs ]){
this[$stubs].push([method,args]);
return true;
}
else
return this[method](...arguments);
},
on(eventName , cb){
if (eventName == 'response'){
if (!cb.http2Safe){
eventName = 'http1.response';
arguments[0] = eventName;
}
}
if (this._on){
this._on(...arguments);
}
else
this.genericStubber('on' , arguments);
},
once(eventName , cb){
if (eventName == 'response'){
if (!cb.http2Safe){
eventName = 'http1.response';
}
}
if (this._once){
this._once(...arguments);
}
else
this.genericStubber('once' , arguments);
},
emitError(error) {
if (this[$stubs]){
this[$stubs].forEach(([method, args]) => {
if ((method === 'on' || method === 'once') && args[0] === 'error') {
args[1](error);
}
});
}
else
return this.emit('error', error);
},
take(stream){
//We forward all functions to the stream
for (var i=0;i<STUBBED_METHODS_NAME.length;i++){
let name = STUBBED_METHODS_NAME[i];
if (stream[name]){
this[name] = stream[name].bind(stream);
}
// else{
// throw new Error(`for stub ${name} no original method found`)
// }
}
this._on = stream.on.bind(stream);
this._once = stream.once.bind(stream);
this.proxyProps(stream);
//This should come later in case of user exception
//We trigger the all the stubs that were generted before
for (let i = 0; i<this[$stubs ].length;i++){
var stub = this[$stubs ][i];
stream[stub[0]](...stub[1]);
}
this[$stubs] = null;
},
proxyProps(http2Stream){
function getter(){
return http2Stream[this];
}
function setter(value){
http2Stream[this] = value;
}
const notToProxy = ['on' , '_on','_once' , 'once','http2Mimic'].concat(STUBBED_METHODS_NAME);
const keys = Object.keys(this);
const keysToProxy = [].concat(PROPERTIES_TO_PROXY);
keys.forEach(function whichProxyKeys(key){
if (notToProxy.indexOf(key) == -1 && keysToProxy.indexOf(key)==-1){
keysToProxy.push(key)
}
});
const properties = Object.getOwnPropertyDescriptors(http2Stream);
for (var i=0;i<keysToProxy.length;i++){
let name = keysToProxy[i];
const propConfig = properties[name];
let shouldCopyValue;
if (!propConfig)
shouldCopyValue = true;
if (propConfig && (propConfig.writable || propConfig))
shouldCopyValue = true;
if (shouldCopyValue)
http2Stream[name] = this[name];
Object.defineProperty(this , name , {
get : getter.bind(name),
set : setter.bind(name),
})
}
}
}
class HttpRequestManager extends EventEmitter{
constructor(options){
super();
this.httpsAgent = https.globalAgent;
this.httpAgent = http.globalAgent;
this.init(options);
}
log(){
}
init(options){
options = options || {};
this.http2Clients = {};
this.cachedHTTP1Result = {};
this.setModules();
this.http2Debouncer = new DebounceTimers(function stopConnection(key){
this.log('stopping ' , key);
var foundConnection = this.http2Clients[key];
if (foundConnection){
this.removeHttp2Client(key , foundConnection)
}
}.bind(this) , 1000);
this.keepH1IdentificationCacheFor = options.keepH1IdentificationCacheFor || 30000;
//the debouncer will accept only values greater then zero
this.http2Debouncer.setDelay(options.keepH2ConnectionFor);
if (options.useHttp){
this.enforceProtocol = 'http:';
}
else if(options.useHttps){
this.enforceProtocol = 'https:';
}
}
setModules() {
this['http'] = require('http');
this['https'] = require('https');
this['tls'] = require('tls');
this['net'] = require('net');
this.http2Support = false;
try{
this['http2'] = require('http2');
this.http2Support = true;
}
catch(err){
//It will automatically fallback to http
}
}
handleClientRequest(clientRequest , requestOptions ,cb){
const requestManager = this;
const clientKey = requestManager.getClientKey(requestOptions);
if (requestManager.hasCachedConnection(clientKey)){
const socket = requestManager.getHttp2Client(clientKey);
const connectionOptions = {
createConnection(){
return socket;
}
};
process.nextTick(function onMakeRequest(){
requestManager.makeRequest( clientRequest ,clientKey , requestOptions, cb , connectionOptions);
}.bind(requestManager))
}
else
requestManager.holdConnectionToIdentification(clientKey , requestOptions , function onIdentification(error , connectionOptions){
if (error) {
clientRequest.emitError(error);
return;
}
requestManager.makeRequest(clientRequest , clientKey , requestOptions, cb , connectionOptions);
}.bind(requestManager));
}
getClientKey(url){
return `${url.protocol || this.enforceProtocol}${url.servername || url.host || url.hostname}:${url.port}`;
}
getHttp2Client(clientKey){
return this.http2Clients[clientKey];
}
setHttp2Client(clientKey , client){
const httpManager = this;
const prevClient = httpManager.http2Clients[clientKey];
if (prevClient)
httpManager.removeHttp2Client(clientKey , prevClient);
httpManager.http2Clients[clientKey] = client;
function closeClient(){
httpManager.removeHttp2Client(clientKey , client);
}
client.on('close' , closeClient);
client.on('goaway' , closeClient);
client.on('error' , closeClient);
client.on('frameError' , closeClient);
client.on('timeout' , closeClient);
}
removeHttp2Client(clientKey , client){
try{
delete this.http2Clients[clientKey];
if (!client.closed){
client.close();
}
}
catch(err){
}
client.removeAllListeners('close');
client.removeAllListeners('error');
client.removeAllListeners('frameError');
client.removeAllListeners('timeout');
}
request(url, options, cb){
var args = new RequestInternalEnforce(arguments);
if (this.enforceProtocol){
args.protocol = this.enforceProtocol;
}
return new ClientRequest(args)
}
get(){
var args = new RequestInternalEnforce(arguments);
args.method = 'GET';
var request = this.request(args);
request.end();
return request;
}
hasCachedConnection(clientKey){
const http2Client = this.getHttp2Client(clientKey);
if (http2Client){
return true;
}
return this.cachedHTTP1Result[clientKey] + this.keepH1IdentificationCacheFor < Date.now();
}
makeRequest(inStream , clientKey , requestOptions ,cb , connectionOptions){
const http2Client = this.getHttp2Client(clientKey);
if (http2Client){
return this.makeHttp2Request(clientKey , inStream , http2Client ,Object.assign(connectionOptions || {}, requestOptions), cb);
}
//It's http1.1 let Node.JS core manage it
if (!requestOptions.agent){
if (requestOptions.protocol == 'https:')
requestOptions.agent = this.httpsAgent;
else
requestOptions.agent = this.httpAgent;
}
return this.makeHttpRequest(clientKey , inStream , requestOptions ,cb , connectionOptions);
}
holdConnectionToIdentification(clientKey , requestOptions , cb){
const topic = `identify-${clientKey}`;
//If there are any pending identification process let's wait for one to finish
if (this._events[topic])
this.once(topic , cb); //There is.. let's wait
else{
//We will need to start identification
this.once(topic , function letKnowThereIsAnEvent(){}); //There is.. let's wait
const socket = this.identifyConnection(requestOptions , function onIdentify(error , type){
if (error) {
return cb(error);
}
var options = {
createConnection(){
return socket;
}
}
if ( type == 'h2' && this.http2Support){
var http2Client = this.http2.connect(requestOptions ,options);
this.setHttp2Client(clientKey , http2Client);
}
else{
//This is http1.1
//Cache last result time
this.cachedHTTP1Result[clientKey] = Date.now();
//Continue let core handle http1.1
}
cb(null, options);
this.emit(topic , options);
}.bind(this))
}
}
makeHttpRequest(clientKey , inStream , options , cb , connectionOptions){
if (options instanceof URL)
options = urlToOptions(options);
const h1op = _extend({} , options);
if (connectionOptions)
h1op.createConnection = connectionOptions.createConnection;
const requestModule = h1op.protocol == 'https:' ? this.https : this.http;
const req = requestModule.request(h1op ,cb);
inStream.take(req);
inStream._on('response' , function onHttp1Response(v){this.emit('http1.response' , v)})
}
makeHttp2Request(clientKey , inStream , http2Client , requestOptions , cb){
var http2Debouncer = this.http2Debouncer;
http2Debouncer.pause(clientKey);
var headers = _extend({} , requestOptions.headers || {});
if (requestOptions.method)
headers[':method'] = requestOptions.method;
if (requestOptions.path)
headers[':path'] = requestOptions.path;
Object.keys(headers).forEach((key)=>{
if (HEADERS_TO_REMOVE.indexOf( (key+'').toLowerCase() ) !=-1){
delete headers[key];
}
})
requestOptions.headers = headers;
var req = http2Client.request(
headers
);
inStream.emit('socket' , requestOptions.createConnection());
let maxContentLength;
let currentContent = 0;
req.on('data' , function onData(data){
currentContent+=data.length;
if (currentContent>= maxContentLength)
http2Debouncer.unpauseAndTime(clientKey);
})
inStream.take(req);
function onResponse(headers){
maxContentLength = parseInt(headers['content-length']);
if (maxContentLength < 0 )
this.http2Debouncer.unpauseAndTime(clientKey);
HttpRequestManager.httpCompatibleResponse(req , requestOptions , headers);
inStream.emit('http1.response' , req);
if (cb)
cb(req);
}
onResponse.http2Safe = true;
req.once('response' , onResponse.bind(this));
}
static httpCompatibleResponse(res , requestOptions , headers){
res.httpVersion = '2.0';
res.rawHeaders = headers;
res.headers = headers;
res.statusCode = headers[':status'];
delete headers[':status'];
}
identifyConnection(requestOptions , cb){
var socket = this.connect( requestOptions, { allowHTTP1: true }, function onConnect(){
socket.removeListener('error', cb);
if (socket.alpnProtocol == 'h2'){
cb(null, 'h2')
}
else{
//close http1.1 connection is it cannot be reused
socket.end();
cb(null, 'h1')
}
});
socket.on('error', cb);
return socket;
}
connect(authority, options, listener) {
if (typeof options === 'function') {
listener = options;
options = undefined;
}
assertIsObject(options, 'options');
options = Object.assign({}, options);
if (typeof authority === 'string')
authority = new URL(authority);
assertIsObject(authority, 'authority', ['string', 'Object', 'URL']);
var protocol = authority.protocol || options.protocol || (this.enforceProtocol != 'detect' ? this.enforceProtocol : null) || 'http:';
var port = '' + (authority.port !== '' ?
authority.port : (authority.protocol === 'http:' ? 80 : 443));
var host = authority.hostname || authority.host || 'localhost';
var socket;
if (typeof options.createConnection === 'function') {
socket = options.createConnection(authority, options);
} else {
switch (protocol) {
case 'http:':
socket = this.net.connect(port, host , listener);
break;
case 'https:':
socket = this.tls.connect(port, host, initializeTLSOptions.call(this , options, host) , listener);
break;
default:
throw new Error('Not supprted' + protocol);
}
}
return socket;
}
}
function urlToOptions(url) {
var options = {
protocol: url.protocol,
hostname: url.hostname,
hash: url.hash,
search: url.search,
pathname: url.pathname,
path: `${url.pathname}${url.search}`,
href: url.href
};
if (url.port !== '') {
options.port = Number(url.port);
}
if (url.username || url.password) {
options.auth = `${url.username}:${url.password}`;
}
return options;
}
class RequestInternalEnforce{
constructor(args){
if (args[0] instanceof RequestInternalEnforce){
return args[0];
}
this.args = args;
this.method = null;
this.protocol = null;
}
}
class HttpsRequest extends HttpRequestManager{
constructor(){
super(...arguments);
this.Agent = https.Agent;
this.globalAgent = https.globalAgent;
this.enforceProtocol = 'https:';
}
}
const httpsRequestSinglton = new HttpsRequest;
HttpsRequest.globalManager = httpsRequestSinglton;
HttpsRequest.Manager = HttpsRequest;
class HttpRequest extends HttpRequestManager{
constructor(){
super(...arguments);
this.Agent = http.Agent;
this.globalAgent = http.globalAgent;
this.enforceProtocol = 'http:';
}
}
const httpRequestSinglton = new HttpRequest;
HttpRequest.globalManager = httpRequestSinglton;
HttpRequest.Manager = HttpRequest;
const singeltonHttpManager = new HttpRequestManager();
singeltonHttpManager.enforceProtocol = 'detect';
HttpRequestManager.globalManager = singeltonHttpManager;
module.exports = {
HttpRequest,
HttpsRequest,
HTTP2OutgoingMessage : ClientRequest,
ClientRequest,
HttpRequestManager
}
+67
View File
@@ -0,0 +1,67 @@
class DebounceTimers{
constructor(cb , defaultDelay){
this.cb = cb;
this.delay = defaultDelay;
this.timers = {
};
this.pausers = {};
}
setDelay(delay){
if ( delay >= 0)
this.delay = delay;
}
pause(key){
this.pausers[key] = this.pausers[key]|| 0;
this.pausers[key]++;
}
unpause(key){
var count = this.pausers[key] || 0;
if (count>0)
count--;
this.pausers[key] = count;
}
unpauseAndTime(key){
this.unpause(key);
this.time(key);
}
time(key){
var self = this;
var timers = this.timers;
var timer = this.timers[key];
if (this.pausers[key] > 0)
return;
if (timer)
clearTimeout(timer);
timers[key] = setTimeout(function onTimer(){
self.cb(key);
delete timers[key];
} , self.delay)
}
}
class ERR_INVALID_ARG_TYPE extends TypeError{
constructor(name, expected, actual){
const type = name.includes('.') ? 'property' : 'argument';
let msg = `The "${name}" ${type} ${determiner} ${expected}`;
}
}
function assertIsObject(value, name, types = 'Object') {
if (value !== undefined &&
(value === null ||
typeof value !== 'object' ||
Array.isArray(value))) {
const err = new ERR_INVALID_ARG_TYPE(name, types, value);
Error.captureStackTrace(err, assertIsObject);
throw err;
}
}
module.exports = {
ERR_INVALID_ARG_TYPE,
assertIsObject,
DebounceTimers
}
+38
View File
@@ -0,0 +1,38 @@
{
"name": "http2-client",
"version": "1.3.5",
"description": "Drop-in replacement for Nodes http and https that transparently make http request to both http1 / http2 server, it's using the ALPN protocol",
"main": "lib/index.js",
"scripts": {
"test": "mocha --recursive --reporter spec",
"watch-test": "mocha --recursive --watch --reporter spec",
"test-ci": "istanbul cover ./node_modules/mocha/bin/_mocha test/**/*.js --report lcovonly -- -R spec && cat ./coverage/lcov.info"
},
"devDependencies": {
"chai": "^4.3.4",
"chai-spies": "^1.0.0",
"http2-debug": "1.2.1",
"istanbul": "0.4.5",
"mocha": "^9.0.2",
"request": "^2.88.2"
},
"keywords": [
"http2",
"http2 client",
"request",
"compatible",
"client",
"compatibility"
],
"repository": {
"type": "git",
"url": "https://github.com/hisco/http2-client.git"
},
"bugs": {
"url": "https://github.com/hisco/http2-client/issues"
},
"homepage": "https://github.com/hisco",
"author": "Eyald <hisco@googlegroups.com>",
"license": "MIT",
"dependencies": {}
}
+772
View File
@@ -0,0 +1,772 @@
const { HttpRequestManager , request , get , http , https} = require('../../lib/index');
const requestModule = require('request');
const {Http2Debug} = require('http2-debug');
const chai = require('chai');
const expect = chai.expect;
chai.use(require('chai-spies'));
const httpModule = require('http');
const SERVER_HOST = '0.0.0.0';
const HTTP_PORT = 8080;
const HTTP2_PORT = 8443;
const HTTP_URL = `http://${SERVER_HOST}:${HTTP_PORT}`;
const HTTP2_URL = `https://${SERVER_HOST}:${HTTP2_PORT}`;
const serverCloseActions = [];
const onHttpServerReady = new Promise((resolve , reject)=>{
try{
const server = httpModule.createServer((req, res) => {
getBody(req)
.then((bodyRaw)=>{
const body = JSON.parse(bodyRaw ? bodyRaw : "{}");
const headers = req.headers;
if (req.url.indexOf('delay')!=-1){
setTimeout(respond,100);
}
else respond();
function respond(){
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
path : req.url,
method : req.method,
body,
headers
}));
}
})
.catch((err)=>{
res.status(500).end('')
})
});
server.listen(HTTP_PORT,SERVER_HOST, (err) => {
if (err)
return reject(err);
serverCloseActions.push(server.close.bind(server));
resolve()
});
}
catch(err){
reject(err);
}
});
const onHTTP2ServerReady = new Promise((resolve , reject)=>{
class HTTP2StubServer extends Http2Debug{
onStream(stream , headers){
const args = arguments;
if (headers[':path'].indexOf('delay')!=-1){
setTimeout(()=> super.onStream.apply(this,args),100 )
}
else{
super.onStream.apply(this,args);
}
}
}
http2Debug = new HTTP2StubServer;
http2Debug.createServer((err)=>{
if (err)
return reject(err);
resolve();
serverCloseActions.push(http2Debug.stopServer.bind(http2Debug));
});
})
describe('e2e' , ()=>{
before(()=>{
return Promise.all([
onHTTP2ServerReady,
onHttpServerReady
])
})
describe('third party validation' ,()=>{
let requestWithHttp2Client;
before(()=>{
requestWithHttp2Client = requestModule.defaults({
httpModules : {
'http:' : http,
'https:' : https,
}
});
})
it('http1 request module should be able to make post request with json body' , async ()=>{
return new Promise((resolve,reject)=>{
requestWithHttp2Client({
uri : `${HTTP_URL}/test1`,
method : 'post',
json:{
name : 'test1'
},
headers : {
'tesT-me' :'90'
}
}, function (error, response, body) {
if (error)
return reject(err);
try{
expect(body.body.name).eq('test1');
expect(body.headers['test-me']).eq('90');
expect(response.statusCode).eq(200);
expect(response.body.method).eq('POST');
resolve();
}
catch(err){
reject(err);
}
});
})
})
it('http2 request module should be able to make post request with json body' , async ()=>{
return new Promise((resolve,reject)=>{
requestWithHttp2Client({
uri : `${HTTP2_URL}/test1`,
method : 'post',
json:{
name : 'test1'
},
headers : {
'tesT-me' :'90'
}
}, function (error, response, respBody) {
if (error)
return reject(error);
try{
const json = JSON.parse(respBody.body);
expect(json.name).eq('test1');
expect(respBody.headers['test-me']).eq('90');
expect(response.statusCode).eq(200);
expect(respBody.headers[':method']).eq('POST');
resolve();
}
catch(err){
reject(err);
}
});
})
})
})
describe('request' , ()=>{
describe('http1' , ()=>{
it('Should be able to make request with request options as a string' , ()=>{
return new Promise((resolve , reject)=>{
const req = request(`${HTTP_URL}/test1` , (res)=>{
getBody(res)
.then((bodyRaw)=>{
const json = JSON.parse(bodyRaw);
expect(res.statusCode).eq(200);
expect(json.path).eq('/test1');
expect(json.method).eq('GET');
resolve();
})
.catch((err)=>{
reject(err)
})
});
req.end();
})
});
it('Should be able to make request with request options and url as a string' , ()=>{
return new Promise((resolve , reject)=>{
const req = request(`${HTTP_URL}/test1` , { method : 'POST'}, (res)=>{
getBody(res)
.then((bodyRaw)=>{
const json = JSON.parse(bodyRaw);
expect(res.statusCode).eq(200);
expect(json.path).eq('/test1');
expect(json.method).eq('POST');
expect(json.body.test).eq(1);
resolve();
})
.catch((err)=>{
reject(err)
})
});
req.write('{"test":1}')
req.end();
})
});
it('Should be able to make request with request options and method lowercase' , ()=>{
return new Promise((resolve , reject)=>{
const req = request({
path : '/test1',
host : SERVER_HOST,
port : HTTP_PORT,
method : 'post'
} , (res)=>{
getBody(res)
.then((bodyRaw)=>{
const json = JSON.parse(bodyRaw);
expect(res.statusCode).eq(200);
expect(json.path).eq('/test1');
expect(json.method).eq('POST');
resolve();
})
.catch((err)=>{
reject(err)
})
});
req.end();
})
});
it('Should be able to make request with request options and headers' , ()=>{
return new Promise((resolve , reject)=>{
const req = request({
path : '/test1',
host : SERVER_HOST,
port : HTTP_PORT,
method : 'delete',
headers : {
'tesT-me' :'90'
}
} , (res)=>{
getBody(res)
.then((bodyRaw)=>{
const json = JSON.parse(bodyRaw);
expect(json.headers['test-me']).eq('90');
expect(res.statusCode).eq(200);
expect(json.path).eq('/test1');
expect(json.method).eq('DELETE');
resolve();
})
.catch((err)=>{
reject(err)
})
});
req.end();
})
});
it('Should be able to abort immediately' , ()=>{
return new Promise((resolve , reject)=>{
const req = request({
path : '/delay',
host : SERVER_HOST,
port : HTTP_PORT,
method : 'delete',
headers : {
'tesT-me' :'90'
}
} , (res)=>{
reject(new Error('Rejected request shouldn\'t respond'))
});
req.on('error' , (err)=>{
try{
expect(err.code).eq('ECONNRESET');
resolve();
}
catch(err){
reject(err);
}
})
req.end();
req.setTimeout(1,()=>{
try{
req.abort();
resolve();
}
catch(err){
reject(err)
}
})
})
});
it('Emits network error on client request' , ()=>{
return new Promise((resolve , reject)=>{
const req = request({
path : '/',
host : SERVER_HOST,
port : 54321,
method : 'GET'
});
req.on('error', (err)=>{
try{
expect(err.code).eq('ECONNREFUSED');
resolve();
}
catch(err){
reject(err);
}
});
req.end();
setTimeout(reject, 100);
})
});
})
describe('http2' , ()=>{
it('Should be able to make request with request options with body' , ()=>{
return new Promise((resolve , reject)=>{
const req = request({
path : '/test1',
protocol : 'https:',
host : SERVER_HOST,
port : HTTP2_PORT,
method : 'POST',
headers : {
'test-me' : 90
}
} , (res)=>{
getBody(res)
.then((bodyRaw)=>{
const json = JSON.parse(bodyRaw);
expect(json.headers['test-me']).eq('90');
expect(res.statusCode).eq(200);
expect(json.headers[':path']).eq('/test1');
expect(json.headers[':method']).eq('POST');
expect(JSON.parse(json.body).test).eq(1);
resolve();
})
.catch((err)=>{
reject(err)
})
});
req.write('{"test":1}');
req.end();
})
});
it('Should be able to abort immediately' , ()=>{
return new Promise((resolve , reject)=>{
const req = request({
path : '/delay',
host : SERVER_HOST,
port : HTTP2_PORT,
method : 'delete',
headers : {
'tesT-me' :'90'
}
} , (res)=>{
reject(new Error('Rejected request shouldn\'t respond'))
});
req.on('error' , (err)=>{
try{
expect(err.code).eq('ECONNRESET');
resolve();
}
catch(err){
reject(err);
}
})
req.end();
req.setTimeout(1,()=>{
req.abort()
})
})
});
it('Should be able to make request with request options and url as string' , ()=>{
return new Promise((resolve , reject)=>{
const req = request(HTTP2_URL , {
path : '/test1',
host : SERVER_HOST,
method : 'POST',
protocol : 'https:',
port : HTTP2_PORT,
headers : {
'tesT-me' :'90'
}
} , (res)=>{
getBody(res)
.then((bodyRaw)=>{
const json = JSON.parse(bodyRaw);
expect(json.headers['test-me']).eq('90');
expect(res.statusCode).eq(200);
expect(json.headers[':path']).eq('/test1');
expect(json.headers[':method']).eq('POST');
resolve();
})
.catch((err)=>{
reject(err)
})
});
req.end();
});
});
it('Should be able to make request with request as string' , ()=>{
return new Promise((resolve , reject)=>{
const req = request(`${HTTP2_URL}/test1`, (res)=>{
getBody(res)
.then((bodyRaw)=>{
const json = JSON.parse(bodyRaw);
expect(res.statusCode).eq(200);
expect(json.headers[':path']).eq('/test1');
expect(json.headers[':method']).eq('GET');
resolve();
})
.catch((err)=>{
reject(err)
})
});
req.end();
});
});
});
});
describe('validate http1 interface as assumed' , ()=>{
describe('http1' , ()=>{
it('Should be able to make request with request options as a string' , ()=>{
return new Promise((resolve , reject)=>{
const req = require('http').request(`${HTTP_URL}/test1` , (res)=>{
getBody(res)
.then((bodyRaw)=>{
const json = JSON.parse(bodyRaw);
expect(res.statusCode).eq(200);
expect(json.path).eq('/test1');
expect(json.method).eq('GET');
resolve();
})
.catch((err)=>{
reject(err)
})
});
req.end();
})
});
/*
For now disabled the following test after verified that
It's working on various machines and on travis only it doesn't.
If someone have idea why only travis then help is welcome.
*/
// it('Should be able to make request with request options and url as a string' , ()=>{
// return new Promise((resolve , reject)=>{
// const req = require('http').request(
// `${HTTP_URL}/test1` ,
// { method : 'POST'},
// (res)=>{
// getBody(res)
// .then((bodyRaw)=>{
// const json = JSON.parse(bodyRaw);
// expect(res.statusCode).eq(200);
// expect(json.path).eq('/test1');
// expect(json.method).eq('POST');
// expect(json.body.test).eq(1);
// resolve();
// })
// .catch((err)=>{
// reject(err)
// })
// });
// req.write('{"test":1}')
// req.end();
// })
// });
it('Should be able to make request with request options and method lowercase' , ()=>{
return new Promise((resolve , reject)=>{
const req = require('http').request({
path : '/test1',
host : SERVER_HOST,
port : HTTP_PORT,
method : 'post'
} , (res)=>{
getBody(res)
.then((bodyRaw)=>{
const json = JSON.parse(bodyRaw);
expect(res.statusCode).eq(200);
expect(json.path).eq('/test1');
expect(json.method).eq('POST');
resolve();
})
.catch((err)=>{
reject(err)
})
});
req.end();
})
});
it('Should be able to make request with request options and headers' , ()=>{
return new Promise((resolve , reject)=>{
const req = require('http').request({
path : '/test1',
host : SERVER_HOST,
port : HTTP_PORT,
method : 'delete',
headers : {
'tesT-me' :'90'
}
} , (res)=>{
getBody(res)
.then((bodyRaw)=>{
const json = JSON.parse(bodyRaw);
expect(json.headers['test-me']).eq('90');
expect(res.statusCode).eq(200);
expect(json.path).eq('/test1');
expect(json.method).eq('DELETE');
resolve();
})
.catch((err)=>{
reject(err)
})
});
req.end();
})
});
it('Should be able to abort immediately' , ()=>{
return new Promise((resolve , reject)=>{
const req = require('http').request({
path : '/delay',
host : SERVER_HOST,
port : HTTP_PORT,
method : 'delete',
headers : {
'tesT-me' :'90'
}
} , (res)=>{
reject(new Error('Rejected request shouldn\'t respond'))
});
req.on('error' , (err)=>{
try{
expect(err.code).eq('ECONNRESET');
resolve();
}
catch(err){
reject(err);
}
})
req.end();
req.setTimeout(1,()=>{
req.abort()
})
})
});
it('Should emit' , ()=>{
return new Promise((resolve , reject)=>{
const req = require('http').request({
path : '/delay',
host : SERVER_HOST,
port : HTTP_PORT,
method : 'delete',
headers : {
'tesT-me' :'90'
}
} , (res)=>{
reject(new Error('Rejected request shouldn\'t respond'))
});
req.on('error' , (err)=>{
try{
expect(err.code).eq('ECONNRESET');
resolve();
}
catch(err){
reject(err);
}
})
req.end();
req.setTimeout(1,()=>{
try{
req.abort();
resolve();
}
catch(err){
reject(err)
}
})
})
});
});
});
describe('get' , ()=>{
before(()=>{
return Promise.all([
onHTTP2ServerReady,
onHttpServerReady
])
});
describe('http1' , ()=>{
it('Should be able to make request with request options as a string' , ()=>{
return new Promise((resolve , reject)=>{
const req = get(`${HTTP_URL}/test1` , (res)=>{
getBody(res)
.then((bodyRaw)=>{
const json = JSON.parse(bodyRaw);
expect(res.statusCode).eq(200);
expect(json.path).eq('/test1');
expect(json.method).eq('GET');
resolve();
})
.catch((err)=>{
reject(err)
})
});
req.end();
})
});
it('Should be able to make request with request options and url as a string' , ()=>{
return new Promise((resolve , reject)=>{
const req = get(`${HTTP_URL}/test1` , { method : 'POST'}, (res)=>{
getBody(res)
.then((bodyRaw)=>{
const json = JSON.parse(bodyRaw);
expect(res.statusCode).eq(200);
expect(json.path).eq('/test1');
expect(json.method).eq('GET');
resolve();
})
.catch((err)=>{
reject(err)
})
});
req.end();
})
});
it('Should be able to make request with request options method lowercase' , ()=>{
return new Promise((resolve , reject)=>{
const req = get({
path : '/test1',
host : SERVER_HOST,
port : HTTP_PORT,
method : 'post'
} , (res)=>{
getBody(res)
.then((bodyRaw)=>{
const json = JSON.parse(bodyRaw);
expect(res.statusCode).eq(200);
expect(json.path).eq('/test1');
expect(json.method).eq('GET');
resolve();
})
.catch((err)=>{
reject(err)
})
});
})
});
it('Should be able to make request with request options and headers' , ()=>{
return new Promise((resolve , reject)=>{
const req = get({
path : '/test1',
host : SERVER_HOST,
port : HTTP_PORT,
method : 'delete',
headers : {
'tesT-me' :'90'
}
} , (res)=>{
getBody(res)
.then((bodyRaw)=>{
const json = JSON.parse(bodyRaw);
expect(json.headers['test-me']).eq('90');
expect(res.statusCode).eq(200);
expect(json.path).eq('/test1');
expect(json.method).eq('GET');
resolve();
})
.catch((err)=>{
reject(err)
})
});
})
});
})
describe('http2' , ()=>{
it('Should be able to make request with request options and headers' , ()=>{
return new Promise((resolve , reject)=>{
const req = get({
path : '/test1',
protocol : 'https:',
host : SERVER_HOST,
port : HTTP2_PORT,
method : 'POST',
headers : {
'test-me' : 90
}
} , (res)=>{
getBody(res)
.then((bodyRaw)=>{
const json = JSON.parse(bodyRaw);
expect(json.headers['test-me']).eq('90');
expect(res.statusCode).eq(200);
expect(json.headers[':path']).eq('/test1');
expect(json.headers[':method']).eq('GET');
resolve();
})
.catch((err)=>{
reject(err)
})
});
})
});
it('Should be able to make request with request options and url as string' , ()=>{
return new Promise((resolve , reject)=>{
const req = get(HTTP2_URL , {
path : '/test1',
host : SERVER_HOST,
method : 'POST',
protocol : 'https:',
port : HTTP2_PORT,
headers : {
'tesT-me' :'90'
}
} , (res)=>{
getBody(res)
.then((bodyRaw)=>{
const json = JSON.parse(bodyRaw);
expect(json.headers['test-me']).eq('90');
expect(res.statusCode).eq(200);
expect(json.headers[':path']).eq('/test1');
expect(json.headers[':method']).eq('GET');
resolve();
})
.catch((err)=>{
reject(err)
})
});
});
});
it('Should be able to make request with request as string and body' , ()=>{
return new Promise((resolve , reject)=>{
const req = get(`${HTTP2_URL}/test1`, (res)=>{
getBody(res)
.then((bodyRaw)=>{
const json = JSON.parse(bodyRaw);
expect(res.statusCode).eq(200);
expect(json.headers[':path']).eq('/test1');
expect(json.headers[':method']).eq('GET');
resolve();
})
.catch((err)=>{
reject(err)
})
});
});
});
});
});
after(async ()=>{
return new Promise((resolve)=>{
serverCloseActions.forEach((action)=>{
action();
});
setTimeout(resolve , 100)
})
})
})
function getBody(stream){
return new Promise((resolve , reject)=>{
let bodyRaw = '';
stream.on('data' , (chunk)=>{
bodyRaw+=chunk;
});
stream.on('end',(chunk)=>{
if (chunk)
bodyRaw+=chunk;
resolve(bodyRaw);
});
stream.on('error' , (err)=>{
reject(err)
})
})
}