How to stream to a Promise? - node.js

When I POST data to my app, I write it to a file and share it with the other instances via PUT. I want to return from POST with the status of each stream (the file and the PUTs).
putResults is an array that's part of the enclosing class, meant to hold the results of each request.
How do I collect the responses? I could return an array of Promises from createWriteStreams but then how could I req.pipe to them? Can you stream to a Promise?
post(req, res, next) {
let listeners = this.getWriteStreams();
let c = listeners.length;
for (i = 0; i < c; i++) {
req.pipe(listeners[i]);
}
/* What do I put here to return after all requests finish? */
}
put(req, res, next) {
var fstream = fs.createWriteStream(this.path);
req.pipe(fstream);
req.on('end', () => {
fstream.close();
res.status(201).send("OK");
});
req.on('error', (err) => {
res.status(500).send(err);
});
}
createWriteStreams() {
let listeners = [];
// We always want to save to a file
listeners.push(fs.createWriteStream(this.path).on('close', ()=>{
this.putResults.push({ host: hutil.myHost, status: 201 });
}));
// If there are other servers in current env, send to them, too!
let otherGuys = hostutil.otherServers();
if (otherGuys.length > 0) {
for (i = 0; i < otherGuys.length; i++) {
let opts = {
hostname: hutil.fq(otherGuys[i]),
port: this.port,
path: this.endpoint,
method: 'PUT',
};
let req = https.request(opts, res => {
this.putResults.push({ host: opts.hostname, status: res.statusCode});
});
req.on('error', (e) => {
this.putResults.push({ host: opts.hostname, status: e });
});
listeners.push(req);
}
}
return listeners;
}

Well, in case anyone ever has this question, the key point of knowledge is that an input stream can be passed around as if it has multiple custom spigots on it - opening one doesn't seem to prematurely spray data all over the other places you're placing the hose!
So, since you cannot stream to a Promise, you still stream to the streams, and you can apparently take your time setting them up. Here's my solution: pass the request to the streams wrapped in the promises.
function post(req, res, next) {
let promises = this.streamPromises(req);
Promise.allSettled(promises).then((results) => {
// Remove the Promise container junk - results come in 2 completely different object structures. Great design, jeez. :-\
let noContainer = results.map(item => item.value).filter(i => i != undefined);
noContainer.push(...results.map(item => item.reason).filter(i => i != undefined));
res.status(200).json(noContainer);
}).catch(err => {
log.warn(`POST request for ${this.filename} failed, at least in part: ${err}`)
res.status(200).json({ host: hutil.myHost, status: err });
});
}
function put(req, res, next) {
var fstream = fs.createWriteStream(this.fqFile);
req.pipe(fstream);
req.on('end', () => {
fstream.close();
log.info(`${req.transID} Saved data to ${this.fqFile} sent by ${req.referer}`);
res.status(201).send("OK");
});
req.on('error', (err) => {
log.warn(`${req.transID} Error receiving/saving PUT file ${this.fqFile} sent by ${req.referer}`);
res.status(500).send(err);
});
}
function streamPromises(postReq) {
let listeners = [];
listeners.push(this.streamLocalFrom(postReq)); // add file first
// Send to other servers in the environment
let otherGuys = hosts.peerServers();
if (otherGuys.length > 0) {
for (i = 0; i < otherGuys.length; i++) {
let opts = {
hostname: hosts.fq(otherGuys[i]),
thatHost: otherGuys[i], // ducked this into the object to avoid parsing fq hostname
port: appconfig.port, // we are all listening here
path: this.endpoint,
method: 'PUT',
timeout: 1000,
ca: fs.readFileSync(appconfig.authorityFile)
};
let p = new Promise((resolve, reject) => {
let req = https.request(opts, res => {
log.info(`${this.filename}: Response from ${opts.hostname}:${opts.port}: ${res.statusCode}`);
// let hn = opts.hostname.match(/(.*?)\./)[1] || opts.hostname;
resolve({ host: opts.thatHost, status: res.statusCode });
});
req.on('error', (e) => {
log.warn(`Error piping ${this.filename} to ${opts.hostname}:${opts.port}: ${e}`);
reject({ host: opts.thatHost, status: e });
});
postReq.pipe(req);
});
listeners.push(p);
}
}
return listeners;
}
function streamLocalFrom(postReq) {
return new Promise((resolve, reject) => {
let fileError = false;
let fstream = fs.createWriteStream(this.fqFile);
fstream.on('close', (err) => {
if (!fileError) {
log.info(`Saved data to file at ${this.fqFile}`);
resolve({ host: me, status: 201 });
}
});
fstream.on('error', (err) => {
log.warn(`Could not save ${this.fqFile} because ${err}`);
reject({ host: hutil.myHost, status: 500 });
fileError = true;
fstream.close();
});
postReq.pipe(fstream);
});
}

Related

NodeJS https module create reusable function to send request and handle response dynamically

This question is similar to this older question but I was not able to get the accepted answer to work correctly.
I am using the built-in NodeJS 'https' module to make requests to an external API. NodeJS version 12.
node: 12.16
express: 4.16.1
I was able to get it working with the example code from the documentation.
router.get('/', (req, res, next) => {
const requestOptions = httpCtrl.getReqOptions();
// Working example
// How to separate this logic into reusable function?
const request = https.request(requestOptions, (response) => {
let result = {
status: null,
data: {}
};
let rawData = '';
response.on('data', (chunk) => {
rawData += chunk;
});
response.on('end', () => {
console.log('No more data in response.');
try {
parsedData = JSON.parse(rawData);
result.status = parsedData.status || 200;
result.data = parsedData;
return res.status(result.status).json(result);
} catch (e) {
result.status = 500;
result.data.message = `ERROR: Unable to parse API response`;
result.data.exception = e;
return res.status(result.status).send(result);
}
});
});
request.on('error', (e) => {
result.status = 500;
result.data.message = `ERROR: API response`;
result.data.exception = e;
return res.status(result.status).send(result);
});
request.end();
});
However, I want to break out this logic into a reusable function, and just pass it the request options dynamically.
I tried just creating a synchronous function wrapper and returning the results, but obviously that didn't work because the sync function does not wait for the completion of the async request.
httpCtrl = {};
httpCtrl.createRequest = (requestOptions) => {
// Does not work due to being synchronous, also tried with async await to no avail
const request = https.request(requestOptions, (response) => {
let result = {
status: null,
data: {}
};
let rawData = '';
response.on('data', (chunk) => {
rawData += chunk;
});
response.on('end', () => {
console.log('No more data in response.');
try {
parsedData = JSON.parse(rawData);
result.status = parsedData.status || 200;
result.data = parsedData;
return result;
} catch (e) {
result.status = 500;
result.data.message = `ERROR: Unable to parse NRS Admin API response`;
result.data.exception = e;
return result;
}
});
});
request.on('error', (e) => {
result.status = 500;
result.data.message = `ERROR: API response`;
result.data.exception = e;
return result;
});
request.end();
});
}
router.get('/', (req, res, next) => {
const requestOptions = httpCtrl.setRequestOptions();
const result = httpCtrl.createRequest(requestOptions);
return res.status(result.status).send(result);
});
How can I update this code to be more re-usable?
Transform createRequest function to a promise, promises work like callbacks except they are much better to read.
// *** createReuqest function is a Promise ***
httpCtrl.createRequest = (requestOptions) => {
return new Promise((resolve, reject) => {
const result = {};
// *** http.request function is a Callback ***
const request = http.request(requestOptions, response => {
let rawData = '';
response.on('data', chunk => rawData += chunk);
// resolve the promise when response ends
response.on('end', () => {
result.status = response.status || 200;
result.data = rawData;
resolve(result);
});
});
// or reject on error
request.on('error', e => {
result.status = 500;
result.data = {
message: 'ERROR: API response',
exception: e
};
reject(result);
});
request.end();
});
};
Now we simply call the function and we chain it with then and catch, however, I choose to use async/await to include all asynchronous JavaScript in this example :) async/await is based on promises but with even cleaner markup.
// *** Finally async/await ***
router.get('/', async (req, res) => {
// initial options for testing
const requestOptions = {
hostname: 'www.google.com',
port: 443,
method: 'GET'
};
// await must be in try/catch to properly handle promise's resolve/reject
try {
const response = await httpCtrl.createRequest(requestOptions);
res.status(response.status).send(response.data);
} catch (error) {
res.status(error.status).send(error.data);
}
});
Hope I've helped.

Nodejs http.get(url[, options][, callback]) method type error while including options

I'm developing a simple chatbot on Amazon Alexa. The idea is to know if an item is on menu at a particular store.
function httpGet() {
return new Promise(((resolve, reject) => {
const options = {
headers: {
'auth_key':'xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx'
}
};
http.get("http://example.com/lookup/api/getitemlist?item=cake&storeid=50", options, (resp) => {
let data = '';
resp.on('data', (chunk) => {
data += chunk;
});
resp.on('end', () => {
resolve(JSON.parse(data));
});
}).on('error', (err) => {
reject(err);
});
}));
}
I'm following documentation on nodejs docs
The API works well in postman where the auth_key is passed in header.
Here's the error from amazon cloudwatch
The API responds with error message when the auth_key isnt present. Am I missing something? From reading the documentation. I thought this would work.
GetItemIntentHandler. I have to write more to handle the response. For now I'm only logging it. This is where I call the function httpGet();
const GetItemIntentHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'GetItemIntent';
},
async handle(handlerInput){
const item = handlerInput.requestEnvelope.request.intent.slots.Item.value;
const response = await httpGet();
console.log("response reached us");
console.log(response);
return handlerInput.responseBuilder
.speak(` ${item}`)
.reprompt("What would you like?")
.getResponse();
}
};
I used the http.get(options[, callback]) method to get around.
Updated httpGet function I'm using now.
function httpGet() {
return new Promise(((resolve, reject) => {
var options = {
host: 'example.com',
path: '/lookup/api/getitemlist?item=cake&storeid=50',
port: null,
headers: {
'auth_key':'xxxxxxx-xxxxx-xxxxx-xxxxx-xxxxxxxxxx'
}
};
http.get(options, (resp) => {
let data = '';
resp.on('data', (chunk) => {
data += chunk;
});
resp.on('end', () => {
resolve(JSON.parse(data));
});
}).on('error', (err) => {
reject(err);
});
}));
}

Use Firebase Function with error TypeError

I am very new to Node js, I just want to get the data from extenal xml from a website but I got an error from Firebase Function log TypeError: invalid media type. I think it come from when I try to do this task parseString(xml, function(err, result) { })
Anyone can help me, it will be great:
Here is my code on firebase function:
exports.getRate = functions.https.onRequest((req, res) => {
getRate = () => {
var url = "https://www.vietcombank.com.vn/ExchangeRates/ExrateXML.aspx";
https.get(url, function(res) {
var xml = "";
res.on('error', function(error){
console.log(error, 'get data error');
})
res.on("data", function(chunk) {
xml += chunk;
console.log(xml, 'xml file');
});
res.on("end", function() {
var date = "";
let rateAUD = {
code: 'AUD/VND',
buy: 0,
sell: 0
};
let rateUSD = {
code: 'USD/VND',
buy: 0,
sell: 0
};
parseString(xml, function(err, result) {
console.log(xml, 'xml file');
date = result.ExrateList.DateTime[0];
if (result.ExrateList.Exrate[0].$.CurrencyCode == "AUD") {
rateAUD.buy = result.ExrateList.Exrate[0].$.Buy;
rateAUD.sell = result.ExrateList.Exrate[0].$.Sell;
} else {
console.log("They change the database list");
}
if (result.ExrateList.Exrate[18].$.CurrencyCode == "USD") {
rateUSD.buy = result.ExrateList.Exrate[18].$.Buy;
rateUSD.sell = result.ExrateList.Exrate[18].$.Sell;
} else {
console.log("They change the database list");
}
console.log(rateAUD, rateUSD, 'get data');
uploadDataToServer(date, { rateAUD, rateUSD });
if(err) {
console.log(err);
}
});
});
});
};
function uploadDataToServer(date, { rateAUD, rateUSD }) {
var db = admin.firestore();
let data = { rateAUD, rateUSD };
data.timeStamp = date;
console.log('upload success');
db.collection("liveRate").add(data),then((err)=> {
console.log(err);
});
}
return res.status(200)
.type('application / json')
.send('hello')
});
'
When I run the same code on another Nodejs playground, it works well.
Here is the link: https://repl.it/repls/MaroonSlateblueProfiler
So weird!
Ps: my payment option is ON.
The problem is that the client is sending the server what may or may not be a valid media type in an encoding the server cannot understand (as per the Content-Encoding header the client packaged with the request message).
Please try to set the content-type to xml:
getRate = () => {
var options = {
hostname: "www.vietcombank.com.vn",
port: 443,
path: "/ExchangeRates/ExrateXML.aspx",
headers: {
'Content-Type': 'application/xml'
}
};
https.get(options, function(res) {
...
});
}

How to return a promise from a Hapi handler

I have a very simple Hapi server that returns a Promise that when resolved calls reply(), I understand that this is possible from its documentation: https://hapijs.com/api#route-handler
The problem is that the Promise seems to not get resolved (and consequently I don't receive a response) until I refresh the page a couple of times or after a really long time. The promise returned by check_ips works as expected so it seems to be something related to how Hapi handles it.
const Hapi = require('hapi');
const ping = require('net-ping')
const server = new Hapi.Server({ debug: { request: ['error'] } });
const port = process.env.PORT || 8080;
server.connection({ port: port, host: 'localhost' });
const session = ping.createSession();
check_ips = function(ips) {
var pings = []
for (var i = 0; i < ips.length; i++) {
pings.push(new Promise(function(fulfill, reject) {
session.pingHost(ips[i], function(err, target) {
if (err) {
if (err instanceof ping.RequestTimedOutError)
fulfill(false)
else
reject(err)
} else {
fulfill(true)
}
})
}))
}
return Promise.all(pings).then(function(results) {
response = {}
for (var i = 0; i < ips.length; i++) {
response[ips[i]] = results[i]
}
return(JSON.stringify(response))
})
}
server.route({
method: 'GET',
path: '/check_ips/{ips}',
handler: function(req, reply) {
if (!req.params["ips"]) {
return reply({"error": "No IPs received"})
}
ips = req.params["ips"].split(",")
return check_ips(ips).then(reply)
}
});
server.start(function(err) {
if (err) {
throw err;
}
console.log(`Server running at: ${server.info.uri}`);
});
Unsure whether the question is still relevant for the OP.
The issue probably has nothing to do with Hapijs or Node.js. It seems to be with the rejection handling, i.e. not having a catch in the route.
I've rewritten your function without side effects to make sure it always returns.
const pingHost = (ip, cb) => {
cb(ip % 3 === 0 && {});
};
const check_ips = function (ips) {
return Promise.all(ips.map((ip) => {
return new Promise((fulfill, reject) => {
pingHost(ip, (err, target) => {
if (err) {
if (err instanceof ping.RequestTimedOutError) {
fulfill(false);
} else {
reject(err);
}
} else {
fulfill(true);
}
});
});
}))
.then((results) => {
return ips.reduce((acc, curr, i) => {
return { ...acc, [curr]: results[i] }
}, {});
})
};
Normally it works fine:
check_ips("1,2".split(",")).then(console.log); // {1: true, 2: true}
Then it, expectedly, fails:
check_ips("1,2,3".split(",")).then(console.log)
Adding a catch clause prevents it from failing:
check_ips("1,2,3".split(",")).then(console.log).catch(console.error)
So in order to properly fix it, I'd start investigating what's the error type that being returned by pingHost, that causes Promise to be rejected.

nodejs - How to promisify http.request? reject got called two times

I'm trying to wrap http.request into Promise:
new Promise(function(resolve, reject) {
var req = http.request({
host: '127.0.0.1',
port: 4000,
method: 'GET',
path: '/api/v1/service'
}, function(res) {
if (res.statusCode < 200 || res.statusCode >= 300) {
// First reject
reject(new Error('statusCode=' + res.statusCode));
return;
}
var body = [];
res.on('data', function(chunk) {
body.push(chunk);
});
res.on('end', function() {
try {
body = JSON.parse(Buffer.concat(body).toString());
} catch(e) {
reject(e);
return;
}
resolve(body);
});
});
req.on('error', function(err) {
// Second reject
reject(err);
});
req.write('test');
}).then(function(data) {
console.log(data);
}).catch(function(err) {
console.log(err);
});
If I recieve errornous statusCode from remote server it will call First reject and after a bit of time Second reject. How to make properly so it calls only single reject (I think First reject is proper one in this case)? I think I need to close res myself, but there is no close() method on ClientResponse object.
UPD:
Second reject triggers very rarely - why?
Your code is almost fine. To restate a little, you want a function that wraps http.request with this form:
function httpRequest(params, postData) {
return new Promise(function(resolve, reject) {
var req = http.request(params, function(res) {
// on bad status, reject
// on response data, cumulate it
// on end, parse and resolve
});
// on request error, reject
// if there's post data, write it to the request
// important: end the request req.end()
});
}
Notice the addition of params and postData so this can be used as a general purpose request. And notice the last line req.end() -- which must always be called -- was missing from the OP code.
Applying those couple changes to the OP code...
function httpRequest(params, postData) {
return new Promise(function(resolve, reject) {
var req = http.request(params, function(res) {
// reject on bad status
if (res.statusCode < 200 || res.statusCode >= 300) {
return reject(new Error('statusCode=' + res.statusCode));
}
// cumulate data
var body = [];
res.on('data', function(chunk) {
body.push(chunk);
});
// resolve on end
res.on('end', function() {
try {
body = JSON.parse(Buffer.concat(body).toString());
} catch(e) {
reject(e);
}
resolve(body);
});
});
// reject on request error
req.on('error', function(err) {
// This is not a "Second reject", just a different sort of failure
reject(err);
});
if (postData) {
req.write(postData);
}
// IMPORTANT
req.end();
});
}
This is untested, but it should work fine...
var params = {
host: '127.0.0.1',
port: 4000,
method: 'GET',
path: '/api/v1/service'
};
// this is a get, so there's no post data
httpRequest(params).then(function(body) {
console.log(body);
});
And these promises can be chained, too...
httpRequest(params).then(function(body) {
console.log(body);
return httpRequest(otherParams);
}).then(function(body) {
console.log(body);
// and so on
});
I know this question is old but the answer actually inspired me to write a modern version of a lightweight promisified HTTP client. Here is a new version that:
Use up to date JavaScript syntax
Validate input
Support multiple methods
Is easy to extend for HTTPS support
Will let the client decide on how to deal with response codes
Will also let the client decide on how to deal with non-JSON bodies
Code below:
function httpRequest(method, url, body = null) {
if (!['get', 'post', 'head'].includes(method)) {
throw new Error(`Invalid method: ${method}`);
}
let urlObject;
try {
urlObject = new URL(url);
} catch (error) {
throw new Error(`Invalid url ${url}`);
}
if (body && method !== 'post') {
throw new Error(`Invalid use of the body parameter while using the ${method.toUpperCase()} method.`);
}
let options = {
method: method.toUpperCase(),
hostname: urlObject.hostname,
port: urlObject.port,
path: urlObject.pathname
};
if (body) {
options.headers = {'Content-Length':Buffer.byteLength(body)};
}
return new Promise((resolve, reject) => {
const clientRequest = http.request(options, incomingMessage => {
// Response object.
let response = {
statusCode: incomingMessage.statusCode,
headers: incomingMessage.headers,
body: []
};
// Collect response body data.
incomingMessage.on('data', chunk => {
response.body.push(chunk);
});
// Resolve on end.
incomingMessage.on('end', () => {
if (response.body.length) {
response.body = response.body.join();
try {
response.body = JSON.parse(response.body);
} catch (error) {
// Silently fail if response is not JSON.
}
}
resolve(response);
});
});
// Reject on request error.
clientRequest.on('error', error => {
reject(error);
});
// Write request body if present.
if (body) {
clientRequest.write(body);
}
// Close HTTP connection.
clientRequest.end();
});
}
There are other ways as well but here you can find a simple way to make http.request as a promise or async/await type.
Here is a working sample code:
var http = require('http');
function requestAsync(name) {
return new Promise((resolve, reject) => {
var post_options = {
host: 'restcountries.eu',
port: '80',
path: `/rest/v2/name/${name}`,
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
};
let post_req = http.request(post_options, (res) => {
res.setEncoding('utf8');
res.on('data', (chunk) => {
resolve(chunk);
});
res.on("error", (err) => {
reject(err);
});
});
post_req.write('test');
post_req.end();
});
}
//Calling request function
//:1- as promise
requestAsync("india").then(countryDetails => {
console.log(countryDetails);
}).catch((err) => {
console.log(err);
});
//:2- as await
let countryDetails = await requestAsync("india");
After reading all of these and a few articles, I thought I'd post a sort of "general" solution that handles both http and https:
const http = require("http");
const https = require("https");
const url_obj = require("url");
const request = async (url_string, method = "GET", postData = null) => {
const url = url_obj.parse(url_string);
const lib = url.protocol=="https:" ? https : http;
const params = {
method:method,
host:url.host,
port: url.port || url.protocol=="https:" ? 443 : 80,
path: url.path || "/"
};
return new Promise((resolve, reject) => {
const req = lib.request(params, res => {
if (res.statusCode < 200 || res.statusCode >= 300) {
return reject(new Error(`Status Code: ${res.statusCode}`));
}
const data = [];
res.on("data", chunk => {
data.push(chunk);
});
res.on("end", () => resolve(Buffer.concat(data).toString()));
});
req.on("error", reject);
if (postData) {
req.write(postData);
}
req.end();
});
}
You could use like this:
request("google.com").then(res => console.log(res)).catch(err => console.log(err))
This is heavily inspired by this article, but replaces the hacky url parsing with the built in api.
Hope this help.
const request = require('request');
async function getRequest() {
const options = {
url: 'http://example.com',
headers: {
'Authorization': 'Bearer xxx'
}
};
return new Promise((resolve, reject) => {
return request(options, (error, response, body) => {
if (!error && response.statusCode == 200) {
const json = JSON.parse(body);
return resolve(json);
} else {
return reject(error);
}
});
})
}
It's easier for you to use bluebird api, you can promisify request module and use the request function async as a promise itself, or you have the option of using the module request-promise, that makes you to not working to creating a promise but using and object that already encapsulates the module using promise, here's an example:
var rp = require('request-promise');
rp({host: '127.0.0.1',
port: 4000,
method: 'GET',
path: '/api/v1/service'})
.then(function (parsedBody) {
// GET succeeded...
})
.catch(function (err) {
// GET failed...
});

Resources