A story about HTTP status codes and why you should read documentation

Salma Alam-Naylor - Nov 11 '23 - - Dev Community

Since 2020, I’ve been working on an Express (Node.js framework) application to power viewer interactions and events that happen whilst I’m streaming live coding on Twitch — my Twitch bot. Since using Sentry for error monitoring and crashes using the Sentry Node SDK, I’ve already squashed quite a few bugs that were entirely a result of my own terrible code. But recently, I triggered a preventable application error via a seemingly failed call to a third-party API (which confused me since it returned an HTTP 200 status code). This reminded me of the importance of sending correct and representative HTTP status codes with responses — especially error responses — and how I should really read documentation better.

What triggered the error?

When streaming on Twitch, you have the ability to “shout out” other streamer accounts. When live, this sends a notification to their dashboard and pins a CTA to the top of your Twitch chat to encourage viewers to follow that streamer. My Twitch bot also extends this experience by sending an announcement to the chat window with the latest stream title and streamer’s category that was shouted out, using various calls to Twitch API endpoints to build up the response.

In twitch chat, I type !so drguthals, which triggers a response from my bot, saying Go check out DrGuthals at twitch.tv/drguthals and give them some panther PEW PEWS. They were last seen streaming: working on the Sentry + Fullstory package in talk shows and podcasts.

Recently when attempting to shout out a Twitch user, I made a typo (I’m only human, after all), and asked my Twitch bot to shout out a user that didn’t exist. The fact that this triggered an issue alert in Sentry (TypeError: Cannot read properties of undefined) highlighted that I had not coded defensively enough around making typos — and this post will detail how I improved upon this. However, through further inspection of the breadcrumb trail that led to this error event, I noticed what I thought was a flaw in the response from the Twitch API.

Breadcrumbs showing the exception caused by TypeError: cannot read properties of undefined, and the previous event being an HTTP request to the Twitch API, asking it to get user data via a login name, and returning an HTTP 200 status code.

The breadcrumb that preceded the TypeError exception was an HTTP request to the Twitch API, requesting user data for a user via a login name string. Clearly, this user didn’t exist, but with the response, the Twitch API returned an HTTP status code 200, indicating that everything was “OK ”. Everything was, in fact, not “OK ”, at least in my application.

I double-checked the non-existence of the login name requested (fa1kfaikfaik) by navigating to twitch.tv/fa1kfaikfaik in a browser — and this was confirmed in the UI. But the network tab also returned an HTTP 200 with the response, when I was expecting an HTTP 404 — not found.

Browser showing twitch user not found, but network tab shows HTTP 200 OK status code.

Often, when dealing with HTTP responses, it’s good practice to check the status code of the response in order to proceed accordingly. For example, you may only want to proceed with sending a welcome email to a new customer if the account was created successfully, and the response returned from the account creation API contained an HTTP status code 201 (created). However, in this case, checking for the HTTP status code would not have solved my problem.

Let’s take a look at the response data in code from this particular request for a user that does not exist. (Notice how Sentry has added the sentry-trace header to the Twitch API call to connect the dots between my application and external HTTP calls when reporting issues.)

Terminal window showing logs adding a sentry-trace header to the Twitch API call, requesting user data for fa1kfaikfaik, and that the response is an object, with one property data, that is an empty array.

Now, technically this HTTP response is “OK ”. We have a response object, containing a property named data, which is an empty array; nothing is intrinsically wrong or malformed. But why was I getting an HTTP 200 when I was expecting an HTTP 404? The answer lies in the design of the Twitch API, and was implied in the shape of the return data logged to the terminal.

I wrote bad code and didn’t read the docs very well

The “/users” endpoint provided by the Twitch API allows you to get information about one or more users. The endpoint is /users, plural, not /user, singular, which is perfectly aligned with RESTful API design. Via this API endpoint, I am able to request data for up to 100 users. If just one out of 100 requests contained a typo and the API completely failed, that would be a pretty poor developer experience.

Despite initially misunderstanding the API endpoint, this reinforces that understanding API design, coding defensively around any constraints, and using an application performance monitoring tool like Sentry can help identify those gaps in your application code.

How I fixed my code

My Twitch bot contains a wrapper function around the Twitch /users API that purposefully only allows my application to request data for one single user. Here’s the original code that caused the TypeError issue in Sentry on an empty response, which makes a call to the Twitch API with the required credentials, and performs an account age check for security reasons. Given the data property of the response from Twitch was empty as shown in the terminal output above, data[0] did not exist when I tried to access the created_at property.

async getUserByLogin(login) {
  const accessTokenData = await accessTokenUtil.get();

  if (accessTokenData) {
    const request = await fetch(
      `https://api.twitch.tv/helix/users?login=${login}`,
      {
        headers: {
          authorization: `Bearer ${accessTokenData.accessToken}`,
          "client-id": process.env.CLIENT_ID,
        },
      },
    );

    const response = await request.json();

    const passesAgeCheck = accountIsOlderThanSevenDays(
      response.data[0].created_at,
    );

    if (!passesAgeCheck) {
       // ...
    }

    return response.data[0];
  }

  return undefined;
}
Enter fullscreen mode Exit fullscreen mode

To make my code more resilient, I’ve now added a check for the length of the data array, and if I make a typo, I send a message to my Twitch chat to call me out on it.

  async getUserByLogin(login) {
    const accessTokenData = await accessTokenUtil.get();

   if (accessTokenData) {
     const request = await fetch(// ...);
     const response = await request.json();

+    if (response.data.length === 0) {
+      tmi.say(config.channel, "Typo: user not found");
+      return undefined;
+    }

   // ...
   return response.data[0];
 }

 return undefined;
}
Enter fullscreen mode Exit fullscreen mode

This may seem obvious, now. But it’s not uncommon. So thanks, Sentry, for calling me out on it, helping me better understand the API I was working with, and for enabling me to make my code more robust. This has also helped to reduce noise in Sentry caused by my inability to type correctly whilst streaming. After all, a typo isn’t an application error; it’s a user error.

Closing thoughts about API design and HTTP status codes

When interacting with APIs, understanding HTTP status codes sent with responses is a useful tool in helping you code defensively against errors in your application. Thorough handling of API responses based on returned HTTP status codes reduces the likelihood of uncaught exceptions, but most importantly, considering the different types of responses and HTTP codes that an API may return, leads to a better understanding of API design itself.

And because I really have seen HTTP 200 status codes returned with legitimate error responses in my career, here are four things to consider when building APIs:

  • Return correct and representative HTTP response status codes with API responses
  • Return consistent and predictable data structures
  • Return organized data — don’t rely on the API consumer to order items unnecessarily, calculate values arbitrarily, or remove items they didn’t ask for
  • Decide when an error really is an error; is an empty response an error, or is it actually “OK”?

And when consuming APIs, don’t do what I did. Read the documentation, understand how the API is designed, and code defensively around unexpected results. And if you really don’t have the time for that, Sentry and Distributed Tracing has your back.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .