Reputation: 55
I am writing a cloud function to start or stop a server for a game panel I'm using. Everything works except when the request completes it never triggers the "data", "end", or "closed" events which cause Firebase to hang. Has anyone else encountered this before? I've googled for "Node http hanging after receiving 204 response" but didn't find anything relevant.
import * as functions from "firebase-functions";
import * as http from "https";
import * as qs from "querystring";
export const commandServer = functions.https.onRequest((request, response) => {
if (request.body.command !== "start" && request.body.command !== "stop") {
response.status(400).send("Error, missing information.");
}
console.log(request.body.origin);
const apiKey = "<apiKey>";
const panelServerId = "<panelId>";
const data = qs.stringify({
signal: request.body.command === "start" ? "start" : "stop",
});
const options = {
protocol: "https:",
hostname: "<server url>",
port: 443,
path: `/api/client/servers/${panelServerId}/power?`,
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Content-Length": data.length,
Authorization: `Bearer ${apiKey}`,
},
};
const req = http.request(options, (res) => {
console.log(`statusCode: ${res.statusCode}`);
res.on("data", () => {
response.status(200).send(`Successfuly sent ${data} command`);
});
});
req.on("error", (error) => {
console.log("error: ", error);
response.status(500).send("Oops, something went wrong!");
});
req.write(data);
req.end();
});
Upvotes: 0
Views: 869
Reputation: 26171
A 204 No Content
response doesn't have any content and thus won't fire any "data"
events. Instead you should be listening for the "end"
event.
Furthermore, there may be more than one "data"
event fired as the returned data is streamed in. Each streamed chunk of data should be combined with the other chunks before being used - such as appending each chunk to a string, a file or passing through a data pipeline.
Here is a corrected example based on the http.get(...)
documentation:
const req = http.request(options, (res) => {
console.log(`statusCode: ${res.statusCode}`);
res.setEncoding('utf8');
let rawData = '';
res.on('data', (chunk) => { rawData += chunk; });
res.on('end', () => {
if (!res.complete) {
// connection lost before complete response was received
response.status(500).send('The connection to the server was terminated while the command was still being sent.');
return;
}
if (res.statusCode < 200 || res.statusCode >= 300) {
// unexpected status code
response.status(500).send('Server responded unexpectedly.');
return;
}
if (rawData === '') {
// no body recieved
response.status(200).send(`Successfuly sent ${data} command and got a ${res.statusCode} response without a body`);
return;
}
// try to parse body as JSON
try {
const parsedData = JSON.parse(rawData);
console.log(`Server gave a ${res.statusCode} response. JSON body: `, parsedData);
response.status(200).send(`Successfuly sent ${data} command and got a ${res.statusCode} response with data: ${JSON.stringify(parsedData)}`);
} catch (error) {
// JSON parse error
console.error(`Failed to parse response body as JSON`, { statusCode: res.statusCode, error, rawData });
response.status(500).send(`Successfuly sent ${data} command and got a ${res.statusCode} response, but the response body could not be parsed as JSON.`);
}
});
});
But as @Renaud provided in their answer, it is often simpler to make use of a third-party request library which handles the specifics for you. It is recommended to choose a library that is built around Promises as these work well within Cloud Functions. Another reason to avoid the base http
and https
libraries is that handling multiple objects called req
/request
and res
/response
can lead to confusion when reviewing the code.
Upvotes: 1
Reputation: 83058
I think that instead of the https
library you should use a library that returns Promises, like axios (see also other options mentioned by samthecodingman in his comment).
The following modifications, with axios, should do the trick (untested):
import * as functions from "firebase-functions";
import * as axios from "axios";
import * as qs from "querystring";
export const commandServer = functions.https.onRequest((request, response) => {
if (request.body.command !== "start" && request.body.command !== "stop") {
response.status(400).send("Error, missing information.");
}
console.log(request.body.origin);
const apiKey = "<apiKey>";
const panelServerId = "<panelId>";
const data = qs.stringify({
signal: request.body.command === "start" ? "start" : "stop",
});
const options = {
url: "<server url>", //Assign the absolute url with protocol, host, port, path, etc... See the doc https://github.com/axios/axios#request-config
method: "post",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Content-Length": data.length,
Authorization: `Bearer ${apiKey}`,
},
};
axios.request(options)
.then(res => {
console.log(`statusCode: ${res.statusCode}`);
response.status(200).send(`Successfuly sent ${data} command`);
})
.catch(err => {
console.log("error: ", error);
response.status(500).send("Oops, something went wrong!");
})
});
Upvotes: 1