A Crime in React Nativeland —The Villain Among Us

Nemanja Stojanovic
Enki Blog
Published in
7 min readJun 30, 2020

--

Note: This story is Part II of the series. For Part I, click here.

The story of the Nightmare on Content Street started of with a parsing error for a particular Enki App Insight:

can't find variable "document"

Such an error seemed out of place in React Nativeland.

The initial attempt of finding the culprit was unsuccessful and the Billion Laughs Hack is eliminated as the suspect.

The story was left of at the minimal example that allowed us to consistently reproduce the issue:

&;

Second Hypothesis

The look of the problematic &; string hints at a parsing bug of an invalid HTML entity.

An HTML entity is a piece of text (“string”) that begins with an ampersand (&) and ends with a semicolon (;) .

What does this tell us?

Before we decide where to go with this, let’s first establish the scene of the crime.

After setting up our Chrome Dev Tools, adding breakpoints along the call stack, and going through a few trial/error exploration loops, we found the exact point of failure.

Credit: https://www.monkeyuser.com/2017/step-by-step-debugging/

Crime location: line 13 in the file decode-entity.browser.js

But why is this code executed in the first place? What events lead up to the crime?

Let’s keep digging.

The file decode-entity.browser.js is located in the parse-entities package, which is a dependency of remark-parse, which is a dependency of our own content parser called @enkidevs/curriculum-parser-markdown.

Note: Shout out to Titus for his amazing OSS work.

Is the problem then in our parser code?

Let’s see:

Nope. The AST is created without an error.

How is this possible? The only code using the “crime scene” file is our markdown parser (confirmed by following the call stack execution path).

Third Hypothesis

Maybe somehow the problem is not the code but the environment in which we’re running it?

Welcome to the second stage of debugging:

  1. That can’t happen
  2. That doesn’t happen on my machine
  3. That shouldn’t happen
  4. Why does that happen?
  5. Oh, I see
  6. How did that ever work?

What would break this code in React Nativeland but not in NodeJSVille?

Let’s test out the parser behavior in a simple React Nativeland town:

A single-component React Native app that tries to reproduce the problem

Does running the code above produce the error?

Successful minimal reproduction of the bug in React Native

Gotcha!

Take a quick stop into the third stage of debugging:

  1. That can’t happen
  2. That doesn’t happen on my machine
  3. That shouldn’t happen
  4. Why does that happen?
  5. Oh, I see
  6. How did that ever work?

Before we continue on into the fourth one.

But why?

The Whodunit

Let’s look at the “crime scene” file decode-entity.browser.js again.

Did we miss any clues?

Hmmm, the browser part in the name seems like an important hint, especially combined with the error about the document variable.

Is this code supposed to run only in the browser? Why is RN executing it then?

Looks like the package parse-entities has a decode-entity.js file as well which appears to be the file we actually want to execute.

So why the mixup?

How does this package signal to the packages using it the difference between these files? How does a user of parse-entities know to use decode-entity.js vs decode-entity.browser.js?

Many NPM libs follow a convention of using fields in package.json to expose files as environment-specific entry points.

For Node.js, the entry point will be the file under the main property and for frontend code we can use the browser property.

Perhaps there’s a React Nativeland specific field as well?

Let’s check package.json of parse-entities.

Voila.

Seems like we’re on the right track.

Let’s now verify this scenario from the other end.

The main bundler in React Nativeland is Facebook’s Metro bundler. Does it use this react-native entry in package.json to determine which file to load for React Native?

Yup, it does (and fallbacks to the browser code otherwise). Here’s the relevant code:

Ok, so both sides match the expectation.

The parse-entities code shown on Github has the react-native field in package.json and the Metro bundler reads the react-native field when loading the code.

Why is then the browser file the one being executed?

Well, what code are we actually running?

Let’s open our node_modules to find the code and see.

Oh-oh, no react-native entry.

If our code doesn’t have the react-native entry, but the code on Github does…maybe we’re not using the latest version of parse-entities?

If we scroll down a bit we can see that our installed version is 1.2.0.

Checking NPM shows that the latest version is 1.2.1 .

Does that patch update contain our fix?

What’s in the 1.2.1 release?

“Fix support React Native” sounds promising!

Let’s verify what’s in it:

https://github.com/wooorm/parse-entities/pull/15

Boom!🎉

We’re officially in the fifth stage of debugging:

  1. That can’t happen
  2. That doesn’t happen on my machine
  3. That shouldn’t happen
  4. Why does that happen?
  5. Oh, I see
  6. How did that ever work?

Time to enter the sixth and final stage, before we fix the problem.

How did this happen?

Well. It didn’t. At least, not initially.

The bug-causing behavior was introduced in version 1.2.0 of parse-entities and snuck up on us.

The discovery also took time because the problem only happens when parsing HTML entities (which is shown by our &; example).

If an Insight being parsed didn’t contain any entities, everything would work fine.

How do we fix it?

Debugging — done ✅

Time for the solution.

The problem is that the version 1.2.0 we have installed breaks the parsing of HTML entities because it loads the wrong file to do the job.

The version 1.2.1 contains the fix that loads the correct file.

This means that the solution is to update our version to the latest.

If we look into our markdown parser, the version of parse-entities we have declared is ^1.1.0 .

The ^ is important here. That means that any minor version within a major version of 1.X.X would get installed, not just the exact 1.1.0.

Here’s how NPM explains it:

Source: https://docs.npmjs.com/misc/semver

This is why our installed version is 1.2.0 even though the declared one is ^1.1.0.

Note: We’re a bit at fault here for not learning from history.

Note: NPM allows you to check if any of your packages is outdated by running npm outdated <package-name>. This will check only the satisfied semver range.

Wait…

Does that mean the fix is to simply…

…run npm install again?

Since the ^1.1.0 captures any release within the major version 1.X.X, it should grab the 1.2.1 as well.

Let’s try it out…

https://www.commitstrip.com/en/2017/12/07/npm-philosophy

Yup.

Reinstalling gives us 1.2.1!

Using our minimal example:

A single-component React Native app that tries to reproduce the problem

Let’s run the barebones app again:

Note: I had to add some flexbox love to make the “Test” string more apparent :)

Yaaaay! 🎉

Everything seems to work.

Note: software reuse is difficult and should be done with great care

What about the original insight?

Conclusion

No matter if you live in the country of React Nativeland, or anywhere else in the Universe of Software, finding the root of a bug can feel intimidating.

There’s many layers of streets to take and avenues to explore.

To track down a bug you must channel your inner Sherlock Holmes and hunt down the problem with rigorous investigation.

That being said, there’s one important aspect that all investigations share.

The simplest solution is usually the right one.

--

--

https://nem035.com — Mostly software. Sometimes I play the 🎷. Education can save the world. @EnkiDevs