My Debugging Adventure: Fixing Bugs In SpaceFeed

My Debugging Adventure: Fixing Bugs In SpaceFeed

Get to know how I fixed some tricky bugs while creating a full stack project called SpaceFeed

ยท

11 min read

This month I created a project called SpaceFeed. It is a social media platform where users can share photos, like and comment on posts, follow and unfollow others, receive notifications and do a lot more

I made use of several technologies like ReactJs, Redux Toolkit, MaterialUI, ExpressJs and Socket.io to create this project. It took me around 15-20 days to finish it and over the journey, I faced some terrible bugs which I will talk about in this article

In this article, I will share

  • How I solved 3 annoying bugs in my project

  • How I implemented certain mechanisms to detect any bug in production

You don't need to be a master of NodeJs and ReactJs to understand this article. Just knowing the basics of how an API is created in NodeJs and what are hooks and components in ReactJs is sufficient!

However, if you don't know these technologies, you can still follow through with this article as I will try my best to keep things simple :)

About SpaceFeed ๐Ÿš€

SpaceFeed is a web app built using the MERN stack! Here, a user can create an account, search and follow users, interact with their posts, share them and do many more things!

Features ๐ŸŽฏ

Some of the features of SpaceFeed are:

  • Authentication: Users can create an account using email, password, profile picture and other details

  • Create Posts: Users can upload posts containing images

  • Interact With Posts: Users can like, comment, share and like a particular comment on a post

  • Follow User: Users can follow and unfollow other users

  • User Suggestions: Users can see a list of suggested users when visiting the profile page

  • Realtime Notifications: Users can receive instant notifications whenever someone likes or comments on their post

  • User Search: Users can search for a person using his name or email

  • News: Users can see who commented on whom's post

Resources ๐Ÿ”—

If you want to learn more about my project, refer to the links below:

  1. Demo Link: https://spacefeed.ishantchauhan.com/

  2. Video Introduction: https://www.youtube.com/watch?v=qTP6rp6s57w&t=50s

  3. GitHub Repository: https://github.com/ishantchauhan710/SpaceFeed/

And also keep in mind that this article is the continuation of my previous article about Debugging Mastery where I talked about different debugging techniques, chrome developer tools, VSCode debugger, OWASP ZAP security testing tool, writing test cases to prevent bugs, debugging in android apps and many more things so do remember to check that out!

Link to the first part of this article: https://ishantchauhan.hashnode.dev/debugging-mastery-the-ultimate-guide-on-fixing-bugs

The 3 Tricky Bugs ๐Ÿ•ท๏ธ

These are the 3 bugs that annoyed me the most but I got something to learn from them!

Also, do remember that to simplify the explanations, I have made some modifications in the following code so it may not completely match with the repository code

White Screen Of Death

My project is divided into multiple components. For instance, I have a main file called App.js which contains <LoginPage/>, <HomePage/>, and <ProfilePage />

Then in <HomePage />, I have 3 components:

  • <ProfileSection /> which calls a function getUser() to fetch user information from the backend and display the user profile picture, followers, followings and total number of posts in the frontend

  • <PostSection /> which calls a function getPosts() to fetch all the posts of people whom the user follows and displays them

  • <SuggestionSection /> which calls a function getNews() to fetch the social news like who commented on whom's post and displays it in the frontend

Similarly, these 3 components have other child components and together my project forms a large nested component tree which is known as the Document Object Model in terms of programming

To fetch the data from the backend and display it, my approach used to be

// PostSection.jsx
const [posts,setPosts] = useState([]);
const getPosts = async () => {
    const response = await axios.get('localhost:5000/posts');
    setPosts(response.data.posts)
}

useEffect(()=>{
    getPosts()
},[])

return (
    posts.map((post) => 
        (<h1>{post.createdBy.username}</h1>)
    );
)

// SuggestionSection.jsx
const getNews = async() => {
    const response = await axios.get('localhost:5000/api/news');
    setPosts(response.data.news)
}

useEffect(()=>{
    getNews()
},[])

return (
    news.map((newsItem) => 
        (<h1>{newsItem.by} commented on {newsItem.on}'s post</h1>)
    );
)

The Bug

Whenever I ran the project, it worked fine however whenever I refreshed it, I used to see a white blank screen. This was because it takes a few seconds to fetch data from API however, reactjs renders the components instantaneously

As there was no data, I used to get error messages like posts.map() is not a function or post.createdBy.username is undefined

The Solution

One solution was to add an && for each map() function. For instance,

COPY

return (
    posts && posts.map((post) => 
        (<h1>{post.createdBy.username}</h1>)
    );
)

// or

return (
    news && news.map((newsItem) => 
        (<h1>{newsItem.by} commented on {newsItem.on}'s post</h1>)
    );
)

However, as I had multiple API calls inside multiple components, it would become time-consuming to make these changes so an easy solution to do this was to move as many API calls as possible to the higher-level components

In other words, instead of calling getNews() and getPosts() inside their respective components, I called them inside their parent components <HomePage /> or App.js itself

const [posts,setPosts] = useState([]);
const [news,setNews] = useState([]);

const getPosts = async() => {
    const response = await axios.get('localhost:5000/api/posts');
    setPosts(response.data.posts)
}

const getNews = async() => {
    const response = await axios.get('localhost:5000/api/news');
    setPosts(response.data.news)
}

useEffect(()=>{
    getPosts()
    getNews()
},[])

return ( 
    <BrowserRouter>
      <Routes>
       <Route path="/home" element={posts && news && <HomePage />} />
      </Routes>
    </BrowserRouter>  
)

The Conclusion

Moving most of your API calls to the parent components or having a common place for making network calls and then conditionally rendering your child components as per the availability of API data can prevent lots of white screen of death related bugs!

Session Handling Bug

A session is a file stored on the server inside the database. It contains information about a particular user and is used to perform actions like uploading a post, commenting on a post, following a user etc.

In the above image, you can see a session existing inside the MongoDb. It points to the userId of logged in user inside the database

A cookie is a file stored inside the web browser. It points to the session id of the user's session inside the backend database. Cookies help in remembering user information. For example, we can keep a user stay logged in even if he closes his browser.

In the above image, you can see how a session cookie named connect.sid is stored inside the browser under the application tab of chrome's developer tools

SpaceFeed makes use of sessions and cookies to authenticate a user. To login a user, the following steps are implemented

  • A user enters his email and password in the login form and clicks on the login button

  • The express server receives the user's credentials and creates a session inside MongoDB

  • Once the session is created, the server returns a 200 OK response with a cookie containing the session id

  • Now, that session id is further used for authorizing the user whenever he creates any post, comments on a post, follows someone etc

The Bug

Whenever a user created an account, his session used to be successfully generated inside the database. However, the server response didn't contain cookies due to which a user couldn't be navigated to the home page (Like in the above image you can see response cookies. During the error, no response cookies were present)

Although the login API worked fine when testing using Postman, however, when using the website in a web browser, the error used to appear

I tried changing the express-session configurations, changing the versions of session-related libraries and even testing the express-session library in a completely new project but the error persisted

This is what my session configuration code looked like:

app.use(
  session({
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    cookie: {
      maxAge: 60 * 60 * 1000,
    },
    rolling: true,
    store: MongoStore.create({
      mongoUrl: process.env.MONGO_CONNECTION_STRING,
    }),
  })
);

The Solution

Luckily, I found on StackOverflow that the issue was related to CORS (Cross Origin Resource Sharing) policy. CORS is a security measure taken by browsers. Under CORS policy, 2 programs running on different addresses cannot communicate with each other.

As my frontend ran on port 3000 and the backend ran on port 5000, I faced this error and the simple solution was to make the following changes in the frontend's package.json file above the "dependencies" section

proxy:"http://localhost:5000/"

The Conclusion

Whenever using session-based authentication in a MERN project, always configure CORS! Either use a proxy in the frontend's package.json file or use the cors middleware in the backend

Image Processing Bug

I wanted that whenever a user created an account, his profile picture should be uploaded to firebase storage using my express API. I succeeded in doing that using a multipart HTML form and multer library however, I had a bug

The Bug

Whenever 2 files had the same name, one would get overwritten by the other file

This was the first time I was using expressjs to communicate with firebase. I was not sure how to rename a file. Although firebase has excellent documentation, Firebase Admin SDK documentation didn't provide enough information

My initial approach was to take the uploaded file in the backend, duplicate it using the Nodejs's fs (file system) module, give it a new name, upload it to firebase and delete it from the backend. I even tried it and successfully failed and now realize how silly this approach was

The Solution

Although you can't rename a file using the multer library but luckily I found a library called multer-firebase-storage which automatically adds a suffix to the file name to ensure that the uploaded images with the same name do not get overwritten. The configurations of that file are as follows:

const storageConfig = Multer({
  storage: FirebaseStorage({
    bucketName: process.env.FIREBASE_STORAGE_BUCKET,
    credentials: fbAdmin.credential.cert(serviceAccount),
    public: true,
    nameSuffix: "_" + generateRandom(), // i.e IMG_8473478349
  }),
  limits: { fileSize: 5000000 },
  fileFilter: (req, file, cb) => {
    if (
      file.mimetype == "image/png" ||
      file.mimetype == "image/jpg" ||
      file.mimetype == "image/jpeg"
    ) {
      cb(null, true);
    } else {
      cb(null, false);
      return cb(
        new createHttpError(
          400,
          "Only PNG, JPG and JPEG format images allowed!"
        )
      );
    }
  },
});

router.post('/create-post',storageConfig.single("file"));

Moreover, instead of using Firebase Admin SDK docs on the firebase website, you can refer to the google cloud platform docs as they are more informative and both of them work nearly the same

The Conclusion

To upload an image on firebase using an express server, one can use a library like multer-firebase-storage to rename the files during the upload phase

This was also another silly bug but never be ashamed of your bugs as they always teach you something new!

Debugging In Production ๐Ÿ”ฅ

There are endless possible causes of bugs in an application. No matter how hard you try, there will be always an undiscovered error especially when you are working on a large-scale project. Even the react library itself has few bugs.

As a result, many times you will see a new bug in production. Debugging in production can be hard therefore I will share 2 strategies I use to keep track of any bugs in production (Especially in the backend)

Using Custom Logging Middleware For Error Detection

Whenever any error occurs in your server, instead of seeing it in the terminal, it would be better if you could see it being constantly recorded in a text file. Well, this is what I am going to show you!

We will create a middleware function that will record all the errors in a .log file. This way, whenever a user gets any error in the backend, you can easily see and fix it!

In your server.js file, write:

const express = require("express");
var fs = require("fs");
var path = require("path");
const app = express();

var accessErrorStream = fs.createWriteStream(
  path.join(__dirname, "error.log"),
  {
    flags: "a",
  }
);

const errorHandlingMiddleware = (err, req, res, next) => {
  accessErrorStream.write(
    `Error: ${JSON.stringify(err)}\nOccured on: ${new Date()}\n\n`
  );
  res.status(400).json({ error: errMessage });
};

app.get('/',(req,res) => {
    throw new Error("This error will be logged in a file called error.log");
})

app.use(errorHandlingMiddleware);
app.listen(5000,() => {console.log("Server started")})

Run the server using node server.js command and in your browser, visit localhost:5000/ and will be able to see the error message inside error.log file!

Code Explanation:

  • var fs = require("fs") refers to the file system module. You can use the fs variable to read and write the files on a computer

  • var accessErrorStream = fs.createWriteStream() creates a writable stream which means it creates a file that can be continuously edited without affecting server performance

  • It takes 2 parameters: The path to store the file and the flags. Flags are just configurations of our writable stream like flags: "a" means we want to append the file or whenever we get a new error, we want to attach it with the previous errors in our file

  • Then we create a middleware called errorHandlingMiddleware where we call accessErrorStream.write() function to insert the errors in our log file

  • Finally we register that middleware using app.use(errorHandlingMiddleware)

You will see all the error logs being logged in an error.log file as shown in the above image!

Using Morgan To Log Traffic

Morgan is a middleware that keeps a track of every traffic entering your server. You can see all the HTTP requests made by them, the browser they are using, the time at which the request was made and many other things!

You can either log all this data in a terminal or save it inside a file. I prefer saving data inside a file. It is very easy to implement morgan in your app,

In the terminal, write:

npm install morgan

Then in your server.js file,

const express = require("express");
const logger = require("morgan");
const app = express();
var accessLogStream = fs.createWriteStream(path.join(__dirname, "access.log"), {flags: "a",});
app.use(logger("combined", { stream: accessLogStream }));

And you are all set! Whenever someone visits your website, you will see his details in a file called access.log (As shown in the above image). By logging traffic, you can keep an eye on any failed requests or malicious attacks

Thanks ๐Ÿ˜‡

I would like to thank Hashnode for providing such an awesome platform to share your thoughts and ideas. If you have any questions, let me know in the comment section or drop me a message on my instagram or linkedin :)

ย