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.
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.
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.
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.)
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;
}
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;
}
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.