The Mysterious Bug in qbreader's Answer Checker, and How I Fixed It
For the longest time, qbreader.org would mysteriously crash once every week or so1 for reasons that I could not explain. Every time I checked the logs, I saw something like this:
|
|
You can see here that the checkAnswer
function, which I spun off into its own npm module, is the culprit here.
The first thing you may ask is what inputs were passed into this function to cause the error.
Great question! I wish I logged this - it would have saved me so much time - but more on that later.
You may think, “this doesn’t seem that bad, clearly tokens[i] is supposed to be a string but isn’t, so just follow the stack trace”. I thought so too, but I was very confused, since before this, I had recently overhauled the code (see commit) in the hopes that (among other things) I could annotate and track the types of everything, causing this bug to disappear.
But, the bug still persisted, so I decided to dig in. In the qb-answer-checker Github repository where the source code lives, I have a bunch of tests that test for correctness. I decided to run my code on those tests, printing everywhere along my code to see where mysterious output could occur.2 I couldn’t find anything, so I tried the next thing:
I ran the checkAnswer
function on all 200,000 tossup answerlines in the database.
I wrote a small program to do so, which I’ve reproduced in full:
|
|
The idea is to create a cursor over the entire collection, checking each answer against its sanitized version (the one with no HTML tags), which should hopefully cover every case of the checkAnswer
code.
It ran, and it ran, and as it approached the 92% mark, I was getting nervous that this wouldn’t work and I’d have to try something else, until it finally crashed on this answerline from 2024 ACF Winter:
|
|
Do you see what’s wrong with it? Yeah, I didn’t either. But, I repeated my procedure from above, inserting console.log statements to figure out the line that failed, until I eventually stumbled upon it:
|
|
ordinalConversions
, which is supposed to be a dictionary, also is an Object, and as such, it comes with some properties that it inherits from its prototype.
In this case, when a string, such as our answerline from above, contains the word “constructor”, instead of skipping over the if block because ordinalConversions
doesn’t contain that as a key, it instead returns the constructor of ordinalConversions
, which is a function!
This explains why we’re getting the type error from earlier - functions don’t have .endsWith
defined on them.
Luckily, the fix is simple: only check for properties that we have defined and excluding ones that we inherit:
|
|
…and repeat it everywhere else I make a similar check. You can view the full commit on Github: 9f2d32b.