A Case Sensitivity Issue That Made Me Debug for an Entire Afternoon

Built a LoL desktop assistant with Tauri + Rust. All skill values were showing 0. Checked the data layer, frontend, backend — finally found the bug hiding in a HashMap key.

· 3 min read

A Case Sensitivity Issue That Made Me Debug for an Entire Afternoon

I built a League of Legends desktop assistant. Tauri + Rust + React, overlay window, real-time champion skill data display.

Once the features were running, skill descriptions were correct, percentages were right. But — every concrete value — damage, cooldown, duration — was 0.

Zero. Not a single exception.


Step one: First, figure out where the data comes from

The architecture has three data paths:

  • LCU → basic info like cooldowns and costs
  • CDragon DataValues → token parsing for skill descriptions
  • CDragon DataValues → values for the stats panel

I first used /grill-with-docs to map out the entire architecture, then wrote unit tests to validate the CDragon parsing layer.

Eight tests, all green.

The parsing layer is fine.


Step two: See what the frontend is actually receiving

Opened devtools console, grabbed the raw JSON returned by get_league_champion_details.

Took one look — every skill had cdragonAvailable: false.

The data was never injected.

But the unit tests for the parsing layer passed. So where did the data go?


Step three: Check the backend logs

Back to the terminal, digging through the Rust-side logs.

bin_data=Some

CDragon data was actually fetched. Backend had the data, but frontend showed false.

The data was lost somewhere in transit.


The real bug: a case sensitivity issue

CDragon’s HashMap stored keys in uppercase: "Q", "W", "E", "R".

LCU returned spellKey in lowercase: "q", "w", "e", "r".

Rust’s HashMap is case-sensitive.

So get_spell("q") always returned None.

Always.


The fix: one line of code

// Before
let slot_bin = bin_data.and_then(|bd| bd.get_spell(slot.as_str()));

// After
let slot_bin = bin_data.and_then(|bd| bd.get_spell(slot.to_uppercase().as_str()));

Added a .to_uppercase().

That’s it.


Lingering minor issues

After the fix, two small annoyances remained:

  1. E ability tick count missing: The NumTicks token wasn’t being rendered correctly into the description text
  2. Stats panel key formatting: CamelCase conversion split AD into A D, which looked really awkward

Neither was fatal, but they bugged me. I’d fix them later, slowly.


Lessons

Green unit tests don’t mean no bugs.

My tests covered the parsing layer — what was stored in the HashMap, how it was parsed. But the calling layer’s case normalization wasn’t tested. The bug was hiding in the gap between the two layers.

These gap bugs are the hardest to find. Because each layer, examined in isolation, looks correct.

bin_data=Some but showing false — you need to read logs layer by layer.

The backend log said the data was there. The frontend log said the data wasn’t. If you only look at one side, everything seems fine. Only by comparing both sides could you discover the data was lost in transit.

CDragon is the only option.

Data Dragon’s data is inaccurate, LCU has no computed values, and there’s no off-the-shelf token parsing library. So CDragon is currently the only reliable League of Legends data source. But its data format isn’t fully aligned with LCU — case sensitivity is one of the pitfalls.

Write the minimal test first, then build large mocks.

Claude Code suggested I write unit tests with real data first, rather than building a complete mock dataset upfront. That turned out to be right — bugs caught by real data would never have surfaced in mocks.


One case sensitivity issue, one entire afternoon.

But at least now I know that HashMap’s .get() is case-sensitive.

In Rust, you only need to learn this lesson once.