src/client/protocol/http.js
const http = require("http");
const https = require("https");
const JsonRpcClientProtocol = require("./base");
const logging = require("../../util/logger");
/**
* Creates an instance of HttpClientProtocol, which has some tweaks from the base class
* required to work with the `node.http` package
*
* @extends JsonRpcClientProtocol
* @requires http
* @requires https
*/
class HttpClientProtocol extends JsonRpcClientProtocol {
/**
* In addition to the params and properties of [JsonRpcClientProtocol]{@link JsonRpcClientProtocol},
* the HttpClientProtocol has the following properties
*
* @property {object} headers HTTP headers passed to the factory instance
* @property {string} encoding Encoding type passed to the factory instance
* @property {('http'|'https')} scheme The scheme to use to connect to the server
*/
constructor(factory, version, delimiter) {
super(factory, version, delimiter);
this.headers = this.factory.headers;
this.encoding = this.factory.encoding;
this.scheme = this.factory.scheme;
}
/**
* Send a message to the server. Sets the request headers passed into `headers`
*
* Calls [listen]{@link JsonRpcClientProtocol#listen} to start listening for recieved data from server.
*
* Ends connection when all data received from the server.
*
* Emits a `serverDisconnected` event when connection is closed.
*
* Throws an error if there was an `error` event received when sending the request
*
* @param {string} request Stringified JSON-RPC message object
* @param {function=} cb Callback function to be called when message has been sent
*/
write(request, cb) {
const options = {
...this.factory.options,
...this.headers
};
this.headers["Content-Length"] = Buffer.byteLength(request, this.encoding);
const responseCallback = (response) => {
if (cb) {
response.on("end", cb);
}
this.listener = response;
this.listen();
};
if (this.scheme === "http") {
this.connector = http.request(options, responseCallback);
} else if (this.scheme === "https") {
this.connector = https.request(options, responseCallback);
} else {
throw Error("Invalid scheme");
}
this.connector.on("close", () => {
this.factory.emit("serverDisconnected");
});
this.connector.on("error", (error) => {
throw error;
});
this.connector.write(request, this.encoding);
this.connector.end();
}
/**
* Setup `this.listener.on("data")` event to listen for data coming into the client.
*
* The HTTP client does not use the delimiter since the completion of a response indicates
* the end of data.
*
* Calls [_waitForData]{@link JsonRpcClientProtocol#_waitForData}
*/
listen() {
this.listener.on("data", (chunk) => {
this.messageBuffer.push(chunk);
});
this.listener.on("end", () => {
if (this.messageBuffer.buffer !== "") {
this._waitForData(this.messageBuffer.emptyBuffer());
}
});
}
/**
* Pass incoming data to [verifyData]{@link JsonRpcClientProtocol#verifyData}
*
* @private
*
*/
_waitForData(data) {
try {
this.verifyData(data);
} catch (e) {
this.gotError(e);
}
}
/**
* Send a notification to the server.
*
* Promise will resolve if the request was sucessfully sent, and reject if
* there was an error sending the request. For the [HttpClientProtocol]{@link HttpClientProtocol}, the resolved promise
* will return the http response object with a `204` response code per the spec.
*
* @param {string} method Name of the method to use in the notification
* @param {Array|JSON} params Params to send
* @return Promise
* @example
* client.notify("hello", ["world"])
*/
notify(method, params) {
return new Promise((resolve, reject) => {
const request = this.message(method, params, false);
try {
this.write(request, () => {
if (this.listener.statusCode === 204) {
resolve({
body: this.listener.body || null,
headers: {
...this.listener.headers
},
statusCode: this.listener.statusCode
});
} else {
reject(new Error("no response receieved for notification"));
}
});
} catch (e) {
// this.connector is probably undefined
reject(e);
}
});
}
/** @inheritdoc */
getResponse(id) {
return {
body: this.responseQueue[id],
headers: {
...this.listener.headers
},
statusCode: this.listener.statusCode
};
}
/** @inheritdoc */
getBatchResponse(batch) {
return {
body: batch,
headers: {
...this.listener.headers
},
statusCode: this.listener.statusCode
};
}
/** @inheritdoc */
rejectPendingCalls(error) {
const err = {
body: error,
headers: {
...this.listener.headers
},
statusCode: this.listener.statusCode
};
try {
this.pendingCalls[err.body.id].reject(err);
this.factory.cleanUp(err.body.id);
} catch (e) {
if (e instanceof TypeError) {
// probably a parse error, which might not have an id
logging
.getLogger()
.error(
`Message has no outstanding calls: ${JSON.stringify(err.body)}`
);
}
}
}
}
module.exports = HttpClientProtocol;