Buttars
Buttars

Reputation: 55

Firebase Cloud function hanging after receiving 204 response code

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

Answers (2)

samthecodingman
samthecodingman

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

Renaud Tarnec
Renaud Tarnec

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

Related Questions