Search code examples
node.jstwitter

Cannot set headers after they are sent to the client on real time tweet streamer socket.io app


I am creating a real time twitter streamer app on node.js and react.js and am facing this error on npm start. I have fixed the package.json file and the app does open up on localhost, however it shuts down immediately as this error pops up. In the package.json, npm run server and npm run client are both triggered concurrently when npm start is entered.

[1] Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
[1]     at new NodeError (node:internal/errors:329:5)
[1]     at ServerResponse.setHeader (node:_http_outgoing:573:11)
[1]     at ServerResponse.header (D:\UCL\real-time-tweet-streamer\node_modules\express\lib\response.js:771:10)
[1]     at ServerResponse.send (D:\UCL\real-time-tweet-streamer\node_modules\express\lib\response.js:170:12) 
[1]     at ServerResponse.json (D:\UCL\real-time-tweet-streamer\node_modules\express\lib\response.js:267:15) 
[1]     at ServerResponse.send (D:\UCL\real-time-tweet-streamer\node_modules\express\lib\response.js:158:21) 
[1]     at D:\UCL\real-time-tweet-streamer\server\server.js:77:9
[1]     at processTicksAndRejections (node:internal/process/task_queues:94:5) {
[1]   code: 'ERR_HTTP_HEADERS_SENT'
[1] }
[1] npm ERR! code 1
[1] npm ERR! path D:\UCL\real-time-tweet-streamer
[1] npm ERR! command failed
[1] npm ERR! command C:\WINDOWS\system32\cmd.exe /d /s /c node server/server.js

Here's the code from server.js

const express = require("express");
const bodyParser = require("body-parser");
const util = require("util");
const request = require("request");
const path = require("path");
const socketIo = require("socket.io");
const http = require("http");

const app = express();
let port = process.env.PORT || 3000;
const post = util.promisify(request.post);
const get = util.promisify(request.get);

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

const server = http.createServer(app);
const io = socketIo(server);

const BEARER_TOKEN = process.env.TWITTER_BEARER_TOKEN;

let timeout = 0;

const streamURL = new URL(
  "https://api.twitter.com/2/tweets/search/stream?tweet.fields=context_annotations&expansions=author_id"
);

const rulesURL = new URL(
  "https://api.twitter.com/2/tweets/search/stream/rules"
);

const errorMessage = {
  title: "Please Wait",
  detail: "Waiting for new Tweets to be posted...",
};

const authMessage = {
  title: "Could not authenticate",
  details: [
    `Please make sure your bearer token is correct. 
      If using Glitch, remix this app and add it to the .env file`,
  ],
  type: "https://developer.twitter.com/en/docs/authentication",
};

const sleep = async (delay) => {
  return new Promise((resolve) => setTimeout(() => resolve(true), delay));
};

app.get("/api/rules", async (req, res) => {
  if (!BEARER_TOKEN) {
    res.status(400).send(authMessage);
  }

  const token = BEARER_TOKEN;
  const requestConfig = {
    url: rulesURL,
    auth: {
      bearer: token,
    },
    json: true,
  };

  try {
    const response = await get(requestConfig);

    if (response.statusCode !== 200) {
      if (response.statusCode === 403) {
        res.status(403).send(response.body);
      } else {
        throw new Error(response.body.error.message);
      }
    }

    res.send(response);
  } catch (e) {
    res.send(e);
  }
});

app.post("/api/rules", async (req, res) => {
  if (!BEARER_TOKEN) {
    res.status(400).send(authMessage);
  }

  const token = BEARER_TOKEN;
  const requestConfig = {
    url: rulesURL,
    auth: {
      bearer: token,
    },
    json: req.body,
  };

  try {
    const response = await post(requestConfig);

    if (response.statusCode === 200 || response.statusCode === 201) {
      res.send(response);
    } else {
      throw new Error(response);
    }
  } catch (e) {
    res.send(e);
  }
});

const streamTweets = (socket, token) => {
  let stream;

  const config = {
    url: streamURL,
    auth: {
      bearer: token,
    },
    timeout: 31000,
  };

  try {
    const stream = request.get(config);

    stream
      .on("data", (data) => {
        try {
          const json = JSON.parse(data);
          if (json.connection_issue) {
            socket.emit("error", json);
            reconnect(stream, socket, token);
          } else {
            if (json.data) {
              socket.emit("tweet", json);
            } else {
              socket.emit("authError", json);
            }
          }
        } catch (e) {
          socket.emit("heartbeat");
        }
      })
      .on("error", (error) => {
        // Connection timed out
        socket.emit("error", errorMessage);
        reconnect(stream, socket, token);
      });
  } catch (e) {
    socket.emit("authError", authMessage);
  }
};

const reconnect = async (stream, socket, token) => {
  timeout++;
  stream.abort();
  await sleep(2 ** timeout * 1000);
  streamTweets(socket, token);
};

io.on("connection", async (socket) => {
  try {
    const token = BEARER_TOKEN;
    io.emit("connect", "Client connected");
    const stream = streamTweets(io, token);
  } catch (e) {
    io.emit("authError", authMessage);
  }
});

console.log("NODE_ENV is", process.env.NODE_ENV);

if (process.env.NODE_ENV === "production") {
  app.use(express.static(path.join(__dirname, "../build")));
  app.get("*", (request, res) => {
    res.sendFile(path.join(__dirname, "../build", "index.html"));
  });
} else {
  port = 3001;
}

server.listen(port, () => console.log(`Listening on port ${port}`));

Would really appreciate it someone could help me understand where the error lies and what possible solutions exist.


Solution

  • After each of the places that you send a response, you need to return so your function doesn't continue executing and cause it to attempt to send another response. I see multiple places in the /api/rules route where you need to add the return after sending a response because if you don't, it executes the res.end(response) even though you've already send a response. That is what causes the error you see.