<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Eshaan Agrawal</title>
    <description>The latest articles on DEV Community by Eshaan Agrawal (@eshaanagrawal).</description>
    <link>https://dev.to/eshaanagrawal</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4007341%2Fece0db01-e244-4b94-8dcf-a35878432bf2.jpg</url>
      <title>DEV Community: Eshaan Agrawal</title>
      <link>https://dev.to/eshaanagrawal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/eshaanagrawal"/>
    <language>en</language>
    <item>
      <title>The Cache Was Working. And Still Causing Duplicate API Calls</title>
      <dc:creator>Eshaan Agrawal</dc:creator>
      <pubDate>Fri, 03 Jul 2026 07:50:03 +0000</pubDate>
      <link>https://dev.to/eshaanagrawal/the-cache-was-working-and-still-causing-duplicate-api-calls-3n51</link>
      <guid>https://dev.to/eshaanagrawal/the-cache-was-working-and-still-causing-duplicate-api-calls-3n51</guid>
      <description>&lt;h1&gt;
  
  
  The Cache Was Working. And Still Causing Duplicate API Calls.
&lt;/h1&gt;

&lt;p&gt;The cache wasn't broken.&lt;br&gt;
Every response was being stored correctly.&lt;br&gt;
Every cache hit was returning instantly.&lt;/p&gt;

&lt;p&gt;And yet — three concurrent requests for the same username were hitting GitHub three times.&lt;/p&gt;

&lt;p&gt;This is the story of a thundering herd bug I found in &lt;a href="https://github.com/JhaSourav07/commitpulse" rel="noopener noreferrer"&gt;CommitPulse&lt;/a&gt; — a high-performance Next.js API that transforms raw GitHub contribution data into 3D isometric SVG streak badges. 106 stars, actively used, real traffic hitting the GitHub API on every badge render and dashboard load.&lt;/p&gt;

&lt;p&gt;The cache worked perfectly for sequential requests. Concurrent ones were a different story.&lt;/p&gt;


&lt;h2&gt;
  
  
  What is CommitPulse
&lt;/h2&gt;

&lt;p&gt;Quick context before the bug.&lt;/p&gt;

&lt;p&gt;CommitPulse takes a GitHub username, fetches contribution data from GitHub's GraphQL API, and renders it as a premium SVG badge. The kind you embed in a README. Every time someone loads a page with your badge, a request fires. If your README gets popular, that's a lot of requests — and GitHub's API has rate limits.&lt;/p&gt;

&lt;p&gt;So CommitPulse has a cache layer. Fetch once, store the result, return it for subsequent requests. Makes sense. The problem was what happened before the first response came back.&lt;/p&gt;


&lt;h2&gt;
  
  
  How I Found It
&lt;/h2&gt;

&lt;p&gt;I was reading through &lt;code&gt;lib/github.ts&lt;/code&gt; tracing the contribution fetch path end-to-end.&lt;/p&gt;

&lt;p&gt;The cache check looked like this — simplified, but this is the pattern across contribution, profile, and repo fetches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/github.ts:254-258&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;contributionsCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchWithRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;GITHUB_GRAPHQL_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// ... process response&lt;/span&gt;
&lt;span class="nx"&gt;contributionsCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check cache. Miss. Fetch from GitHub. Store result. Return.&lt;/p&gt;

&lt;p&gt;That's correct for a single request. But read it again thinking about two requests arriving at the same time for the same username.&lt;/p&gt;

&lt;p&gt;Request A arrives. Checks cache. Miss. Starts fetching from GitHub.&lt;/p&gt;

&lt;p&gt;Request B arrives 5ms later. Checks cache. Miss — because Request A hasn't finished yet, so nothing is stored. Starts fetching from GitHub.&lt;/p&gt;

&lt;p&gt;Request C arrives 10ms later. Same thing. Third GitHub API call for identical data.&lt;/p&gt;

&lt;p&gt;All three requests check the cache, all three see a miss, all three independently hit GitHub. The first one to finish populates the cache. But by then, the other two have already sent their requests.&lt;/p&gt;

&lt;p&gt;The cache only helps after a response is complete. While the first request is still in-flight, every concurrent request for the same key goes straight to GitHub.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Actually Matters
&lt;/h2&gt;

&lt;p&gt;For a badge that's embedded in one README and gets 50 views a day, this is invisible. Fine.&lt;/p&gt;

&lt;p&gt;But CommitPulse serves public badges. A popular README can get thousands of views in a short burst — a Hacker News post, a GitHub trending spike, a deploy cold start where the cache is empty. In those moments, concurrent requests for the same username fire simultaneously, and without in-flight deduplication, every one of them hits GitHub independently.&lt;/p&gt;

&lt;p&gt;GitHub's GraphQL API has rate limits. Authenticated requests get 5,000 points per hour. A complex contribution query costs points. During a cold start with high traffic, this amplifies GitHub API usage proportionally to the number of concurrent requests — not proportionally to the number of unique usernames.&lt;/p&gt;

&lt;p&gt;This is the thundering herd problem. A cache miss under load triggers a flood of identical upstream calls, each one racing to populate the cache with the same data.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix: In-Flight Request Deduplication
&lt;/h2&gt;

&lt;p&gt;The solution is to track pending requests separately from completed cache entries.&lt;/p&gt;

&lt;p&gt;When a cold miss happens and a fetch starts, store the pending Promise in a Map keyed the same way as the cache. Any concurrent request that misses the cache checks the in-flight map next — if there's already a pending request for that key, return the same Promise instead of starting a new one.&lt;/p&gt;

&lt;p&gt;Once the request settles (success or failure), remove it from the in-flight map.&lt;/p&gt;

&lt;p&gt;Here's the implementation for the contributions fetch path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/github.ts — after PR #1589&lt;/span&gt;

&lt;span class="c1"&gt;// in-flight request maps, one per data type&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;contributionsInFlight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;GitHubContributions&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;profileInFlight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;GitHubProfile&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reposInFlight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;GitHubRepo&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchGitHubContributions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;year&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;bypassCache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;GitHubContributions&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;year&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;

  &lt;span class="c1"&gt;// 1. check completed cache first&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;bypassCache&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;contributionsCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. check in-flight map — return existing promise if one is pending&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;bypassCache&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;contributionsInFlight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;contributionsInFlight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. cold miss — start the fetch, register it in the in-flight map&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetchWithRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;GITHUB_GRAPHQL_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;processResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nx"&gt;contributionsCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;contributionsInFlight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// clean up after settling&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="nx"&gt;contributionsInFlight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things happening here:&lt;/p&gt;

&lt;p&gt;First, check the completed cache (unchanged from before). Second, check the in-flight map — if a request is already pending for this key, return that same Promise. All concurrent callers now share one pending request. Third, on a genuine cold miss, create the fetch Promise, register it in the in-flight map, and remove it in &lt;code&gt;finally&lt;/code&gt; once it settles.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;bypassCache&lt;/code&gt; / &lt;code&gt;refresh=true&lt;/code&gt; path intentionally skips both the cache and the in-flight map. An explicit refresh should always be a real fresh request — that's the whole point of a force refresh.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Repro Test
&lt;/h2&gt;

&lt;p&gt;The clearest way to verify this class of bug is to delay the mock fetch slightly and fire concurrent requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// before the fix — this test would fail (fetch called 3 times)&lt;/span&gt;
&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deduplicates concurrent cold cache misses&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;fetchCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

  &lt;span class="nx"&gt;vi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mocked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;mockImplementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;fetchCount&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;// simulate network delay&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;mockGraphQLResponse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// fire three concurrent requests for the same username&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nf"&gt;fetchGitHubContributions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;octocat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;fetchGitHubContributions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;octocat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;fetchGitHubContributions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;octocat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;])&lt;/span&gt;

  &lt;span class="c1"&gt;// should only have hit GitHub once&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fetchCount&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without deduplication: &lt;code&gt;fetchCount&lt;/code&gt; is 3. All three concurrent requests miss the cache before any of them finish.&lt;/p&gt;

&lt;p&gt;With deduplication: &lt;code&gt;fetchCount&lt;/code&gt; is 1. Requests B and C see the pending Promise from Request A and return it directly. GitHub gets one call.&lt;/p&gt;

&lt;p&gt;I added this regression test for all three data paths — contributions, profile, and repos — so the behavior is pinned going forward.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Broader Pattern
&lt;/h2&gt;

&lt;p&gt;This is a specific instance of a pattern called &lt;strong&gt;request coalescing&lt;/strong&gt; — combining multiple identical pending requests into one.&lt;/p&gt;

&lt;p&gt;It comes up whenever you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A cache that only holds completed responses&lt;/li&gt;
&lt;li&gt;High concurrency or bursty traffic&lt;/li&gt;
&lt;li&gt;An upstream resource with rate limits or cost (GitHub API, a database, a paid third-party service)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cache alone isn't enough. You need deduplication at the in-flight level too.&lt;/p&gt;

&lt;p&gt;The in-flight Map approach is the standard fix. It's small — a Map per data type, a few lines of check-and-register logic — and it slots cleanly into existing cache code without changing the API surface or the completed-cache behavior.&lt;/p&gt;

&lt;p&gt;If you're building anything with a cache layer that sits in front of a rate-limited API — check whether your in-flight requests are deduplicated. If they're not, a cold start under load will hit your upstream harder than you expect.&lt;/p&gt;




&lt;h2&gt;
  
  
  While I Was There — Three More Bugs
&lt;/h2&gt;

&lt;p&gt;Since I was deep in &lt;code&gt;lib/github.ts&lt;/code&gt; already, I kept reading. Found three more things worth fixing in the same file:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PR #855&lt;/strong&gt; — &lt;code&gt;getHeaders()&lt;/code&gt; always built an &lt;code&gt;Authorization: bearer ${process.env.GITHUB_PAT || process.env.GITHUB_TOKEN}&lt;/code&gt; header. When neither variable was set, outgoing requests got &lt;code&gt;Authorization: bearer undefined&lt;/code&gt;. Not a valid credential, and the only feedback was a confusing 401 from GitHub. Fixed it to resolve the token first, validate it, and throw a clear config error before making any request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PR #1131&lt;/strong&gt; — &lt;code&gt;fetchWithRetry&lt;/code&gt; added an &lt;code&gt;abort&lt;/code&gt; event listener to the caller's &lt;code&gt;AbortSignal&lt;/code&gt; on every attempt but never removed it after the request settled. On successful requests, retries, and network failures, stale listener references accumulated on long-lived signals. Fixed it with a named callback and a &lt;code&gt;finally&lt;/code&gt; cleanup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PR #1244&lt;/strong&gt; — GitHub REST helpers were inserting raw usernames directly into URL paths: &lt;code&gt;`/users/${username}`&lt;/code&gt;. A username containing &lt;code&gt;/&lt;/code&gt;, &lt;code&gt;?&lt;/code&gt;, or &lt;code&gt;#&lt;/code&gt; reshapes the URL instead of being treated as one path segment. Fixed it with &lt;code&gt;encodeURIComponent(username)&lt;/code&gt; before building REST URLs, with regression tests asserting &lt;code&gt;/&lt;/code&gt; becomes &lt;code&gt;%2F&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Four bugs. One file. Same pattern across all of them — code that works fine for the normal happy path but quietly misbehaves under edge cases or load.&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;PR #1589: &lt;a href="https://github.com/JhaSourav07/commitpulse/pull/1589" rel="noopener noreferrer"&gt;fix(api): dedupe concurrent GitHub cache misses&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PR #855: &lt;a href="https://github.com/JhaSourav07/commitpulse/pull/855" rel="noopener noreferrer"&gt;fix(api): fail fast when GitHub token is missing&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PR #1131: &lt;a href="https://github.com/JhaSourav07/commitpulse/pull/1131" rel="noopener noreferrer"&gt;fix(api): clean up fetch retry abort listeners&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PR #1244: &lt;a href="https://github.com/JhaSourav07/commitpulse/pull/1244" rel="noopener noreferrer"&gt;fix(api): encode GitHub REST username paths&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/JhaSourav07/commitpulse" rel="noopener noreferrer"&gt;JhaSourav07/commitpulse&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Part 3 of my GSSoC 2026 contribution series. Currently #50 globally, S Tier, top 1% of 43,587 contributors across 14 repos.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Next up: Fixing undefined behavior in a Python library's C++ core.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>performance</category>
      <category>opensource</category>
      <category>typescript</category>
    </item>
    <item>
      <title>I Found 4 Bugs in an AI Student Platform — Here's What Was Actually Wrong</title>
      <dc:creator>Eshaan Agrawal</dc:creator>
      <pubDate>Wed, 01 Jul 2026 16:18:35 +0000</pubDate>
      <link>https://dev.to/eshaanagrawal/i-found-4-bugs-in-an-ai-student-platform-heres-what-was-actually-wrong-dof</link>
      <guid>https://dev.to/eshaanagrawal/i-found-4-bugs-in-an-ai-student-platform-heres-what-was-actually-wrong-dof</guid>
      <description>&lt;p&gt;A GET endpoint inserting rows into a production database.&lt;br&gt;
A notification bell hammering an API with requests before the user even signed in.&lt;br&gt;
Failed API calls silently showing up as empty states.&lt;br&gt;
A frontend component expecting &lt;code&gt;{ doubts, pagination }&lt;/code&gt; from an API that returned a plain array.&lt;/p&gt;

&lt;p&gt;Four separate bugs. One codebase. All found in the same sitting.&lt;/p&gt;

&lt;p&gt;This is a writeup of everything I found and fixed in &lt;a href="https://github.com/knoxiboy/DoubtDesk" rel="noopener noreferrer"&gt;DoubtDesk&lt;/a&gt; — an anonymous, AI-powered doubt-solving platform built for students to ask questions without fear and get instant answers. TypeScript, Next.js, Clerk auth, and a Postgres database on the backend.&lt;/p&gt;

&lt;p&gt;I'm writing this because every one of these bugs is something you will build yourself at some point. Not because you're a bad developer — because they're easy to miss and nobody teaches you to look for them.&lt;/p&gt;


&lt;h2&gt;
  
  
  The one that actually surprised me
&lt;/h2&gt;

&lt;p&gt;I'll start with the worst one. Not the most complex. Just the one that made me go back and re-read the code twice because I thought I was misreading it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;src/app/api/notifications/test-seed/route.ts&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// the exported handler&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="c1"&gt;// ...&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;notificationsTable&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dummyNotifications&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;GET&lt;/code&gt; handler. Doing a database insert.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;GET /api/notifications/test-seed&lt;/code&gt; — a standard HTTP GET request — was inserting dummy notification rows for whatever signed-in user hit the endpoint. No &lt;code&gt;NODE_ENV&lt;/code&gt; check. No dev-only gate. Live in normal app builds, accessible in production.&lt;/p&gt;

&lt;p&gt;What that means in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open the URL in a browser — inserts data&lt;/li&gt;
&lt;li&gt;Refresh the page — inserts more data&lt;/li&gt;
&lt;li&gt;A crawler or bot visits the URL — inserts data&lt;/li&gt;
&lt;li&gt;Someone shares the link — inserts data for everyone who clicks it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every visit created new notification rows. No confirmation. No intent. Just visiting a URL.&lt;/p&gt;

&lt;p&gt;And when it failed, the catch block returned this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Seeding failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;details&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stack&lt;/span&gt;  &lt;span class="c1"&gt;// full stack trace to the client&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stack traces in API responses. In a production endpoint.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this happens
&lt;/h2&gt;

&lt;p&gt;Test helpers get written fast. Someone needs dummy data for local development, writes a quick seed route, ships it, and moves on. The app works. Nobody notices the route is still live because in development it's harmless — you're the only one hitting it and the data doesn't matter.&lt;/p&gt;

&lt;p&gt;The gap is that test helpers need the same production discipline as real routes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;gate them behind &lt;code&gt;NODE_ENV === "development"&lt;/code&gt; or remove them entirely&lt;/li&gt;
&lt;li&gt;never expose mutation via GET — GET should be read-only by HTTP convention&lt;/li&gt;
&lt;li&gt;never return &lt;code&gt;error.stack&lt;/code&gt; to the client&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix was straightforward. Block the route in production, switch mutation to POST, strip the stack trace from the error response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// after — PR #451&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Method not allowed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;405&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Not found&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// seeding logic only runs in dev/test&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;notificationsTable&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dummyNotifications&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GET returns 405. POST is the only mutation path. Production returns 404. Error responses return a generic message — no stack, no details.&lt;/p&gt;

&lt;p&gt;I also added Jest coverage for the blocked GET, the successful POST seed, and the production gate. Because a security fix without a test is just a comment promising it won't happen again.&lt;/p&gt;




&lt;h2&gt;
  
  
  The notification bell polling before auth
&lt;/h2&gt;

&lt;p&gt;While fixing the seed route I was already in the notification code, so I opened &lt;code&gt;src/components/NotificationBell.tsx&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Line 31:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSWR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/notifications&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fetcher&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;refreshInterval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unconditional. No auth check. No &lt;code&gt;isLoaded&lt;/code&gt; guard.&lt;/p&gt;

&lt;p&gt;The notification bell starts polling &lt;code&gt;/api/notifications&lt;/code&gt; every 10 seconds from the moment the component renders — before Clerk has confirmed whether the user is signed in, during sign-in, after sign-out, after persistent API failures. It just keeps going.&lt;/p&gt;

&lt;p&gt;Open the network tab and you'd see a stream of requests hitting the notifications endpoint from the second the page loads, regardless of auth state. Each one returning a 401 for a signed-out user. Each one triggering an automatic SWR retry. Repeated 401s, noisy server logs, wasted work — all for a header widget that can't show useful data in that state anyway.&lt;/p&gt;

&lt;p&gt;The fix is gating the SWR key on Clerk auth state. SWR treats a &lt;code&gt;null&lt;/code&gt; key as "don't fetch" — so you pass &lt;code&gt;null&lt;/code&gt; when the user isn't loaded or isn't signed in, and the real URL only when auth is confirmed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// before&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSWR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/notifications&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fetcher&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;refreshInterval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// after — PR #442&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isLoaded&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isSignedIn&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAuth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSWR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;isLoaded&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;isSignedIn&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/notifications&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;fetcher&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;refreshInterval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;onErrorRetry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;revalidate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;retryCount&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;retryCount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;  &lt;span class="c1"&gt;// stop retrying after 3 failures&lt;/span&gt;
      &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;revalidate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;retryCount&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two changes: the SWR key is now conditional on &lt;code&gt;isLoaded &amp;amp;&amp;amp; isSignedIn&lt;/code&gt;, and repeated failures stop retrying after 3 attempts instead of hammering the API forever.&lt;/p&gt;

&lt;p&gt;I also added a retry state — instead of showing an empty notification list when loading fails, the component now shows a "Try again" button. Because an empty list and a failed load are two completely different states and the user deserves to know which one they're looking at.&lt;/p&gt;




&lt;h2&gt;
  
  
  The bug that made failed requests look like empty data
&lt;/h2&gt;

&lt;p&gt;Same theme, different components.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;src/app/bookmarks/page.tsx&lt;/code&gt; and &lt;code&gt;src/components/RecommendedClassrooms.tsx&lt;/code&gt;, failed API requests were being silently swallowed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// bookmarks/page.tsx — before&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/bookmarks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nf"&gt;setBookmarks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// logged, not stored&lt;/span&gt;
  &lt;span class="c1"&gt;// falls through to empty state&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;/api/bookmarks&lt;/code&gt; returned a 500, the catch block logged it and the component fell through to rendering "no bookmarks." Same for recommendations. A user staring at an empty bookmarks page had no way to know whether they had no bookmarks or the app had failed to load them.&lt;/p&gt;

&lt;p&gt;Quick way to verify: temporarily return a 500 from either route. The UI shows "no bookmarks" like everything is fine.&lt;/p&gt;

&lt;p&gt;The fix is checking &lt;code&gt;res.ok&lt;/code&gt; before treating the response as valid data, and keeping an error state separate from empty data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// after — PR #444&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/bookmarks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Failed to load bookmarks (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nf"&gt;setBookmarks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Something went wrong. Try again.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Empty state only shows after a successful empty response. Failed responses show a clear message with a retry button. Two states, two different UIs. Simple.&lt;/p&gt;




&lt;h2&gt;
  
  
  The frontend-API contract mismatch
&lt;/h2&gt;

&lt;p&gt;Last one. This one was pure API contract drift.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;src/components/InfiniteDoubtFeed.tsx&lt;/code&gt; is a reusable infinite scroll component. It was written expecting each SWR page to look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// what InfiniteDoubtFeed expected&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;doubts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Doubt&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;pagination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;hasMore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But &lt;code&gt;src/app/api/doubts/route.ts&lt;/code&gt; returned this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// what the API actually returned&lt;/span&gt;
&lt;span class="nx"&gt;Doubt&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;  &lt;span class="c1"&gt;// plain array&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So &lt;code&gt;page?.doubts&lt;/code&gt; was always &lt;code&gt;undefined&lt;/code&gt;. The feed flattened to an empty list. Pagination never worked. The component had been built for a response shape the API never provided.&lt;/p&gt;

&lt;p&gt;Nobody noticed because the component probably hadn't been wired into a live page yet — but it would have silently shown empty data the moment it was used.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// before — PR #443&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doubts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;doubts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasMore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;pagination&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;hasMore&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="c1"&gt;// after — handles both shapes&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doubts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;doubts&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasMore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Doubt&lt;/span&gt;&lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;pagination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hasMore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pagination&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;hasMore&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The component now handles both the current plain array response and the object shape with pagination — so it works today and keeps working if the API changes later. I also made the local fetcher throw on non-2xx responses so failed loads don't get treated as valid page data.&lt;/p&gt;




&lt;h2&gt;
  
  
  The pattern across all four bugs
&lt;/h2&gt;

&lt;p&gt;These are different bugs but they share the same root cause: &lt;strong&gt;things that work in development silently failing in production&lt;/strong&gt; — or worse, silently misbehaving without failing at all.&lt;/p&gt;

&lt;p&gt;The test seed route worked fine locally because the developer was the only one hitting it. The notification polling worked fine in a signed-in session because that's the only state it was tested in. The empty state bug was invisible until you deliberately broke the API. The contract mismatch was invisible until you wired the component to a real page.&lt;/p&gt;

&lt;p&gt;The fix for all of them is the same mindset: test failure cases, not just happy paths. What happens when the user is signed out? What happens when the API returns a 500? What happens when the response shape doesn't match what the component expects?&lt;/p&gt;

&lt;p&gt;If you're building a Next.js app right now — open your network tab, sign out, and watch what requests fire. You'll probably find something.&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;PR #451: &lt;a href="https://github.com/knoxiboy/DoubtDesk/pull/451" rel="noopener noreferrer"&gt;fix: guard notification test seed route&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PR #442: &lt;a href="https://github.com/knoxiboy/DoubtDesk/pull/442" rel="noopener noreferrer"&gt;fix: gate notification polling by auth&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PR #444: &lt;a href="https://github.com/knoxiboy/DoubtDesk/pull/444" rel="noopener noreferrer"&gt;fix: show errors for failed saved data loads&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PR #443: &lt;a href="https://github.com/knoxiboy/DoubtDesk/pull/443" rel="noopener noreferrer"&gt;fix: normalize infinite doubt feed pages&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/knoxiboy/DoubtDesk" rel="noopener noreferrer"&gt;knoxiboy/DoubtDesk&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Part of my GSSoC 2026 contribution series. Currently #50 globally, S Tier, top 1% of 43,587 contributors. Writing about the bugs that actually taught me something — one post every two weeks.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Next up: fixing concurrent cache misses causing a thundering herd in a Next.js GitHub analytics tool.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>nextjs</category>
      <category>security</category>
      <category>typescript</category>
    </item>
    <item>
      <title>The Bug That Sends "Authorization: bearer undefined" - And Why It's So Easy to Miss</title>
      <dc:creator>Eshaan Agrawal</dc:creator>
      <pubDate>Tue, 30 Jun 2026 22:33:41 +0000</pubDate>
      <link>https://dev.to/eshaanagrawal/the-bug-that-sends-authorization-bearer-undefined-and-why-its-so-easy-to-miss-4h48</link>
      <guid>https://dev.to/eshaanagrawal/the-bug-that-sends-authorization-bearer-undefined-and-why-its-so-easy-to-miss-4h48</guid>
      <description>&lt;h3&gt;
  
  
  &lt;code&gt;Authorization: bearer undefined&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;No error. No stack trace. Just a 401 that looked like it came from nowhere.&lt;/p&gt;

&lt;p&gt;I was poking around &lt;strong&gt;CommitPulse&lt;/strong&gt; locally — it's the open-source project that generates those GitHub streak/contribution SVGs you see embedded in people's READMEs. I hadn't set a token in my &lt;code&gt;.env&lt;/code&gt; yet. Figured I'd see what broke.&lt;/p&gt;

&lt;p&gt;What broke was confusing in a specific way: it didn't fail at startup, it didn't fail with a config error, it just quietly sent a bad request to GitHub's GraphQL API and let GitHub be the one to reject it.&lt;/p&gt;

&lt;h4&gt;
  
  
  Tracing it back
&lt;/h4&gt;

&lt;p&gt;I went looking for where the GitHub client builds its request headers. Found it in &lt;code&gt;lib/github.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/github.ts:185-187 — before&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getHeaders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GITHUB_PAT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. That's the whole bug.&lt;/p&gt;

&lt;p&gt;If neither &lt;code&gt;GITHUB_PAT&lt;/code&gt; nor &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; is set, &lt;code&gt;process.env.GITHUB_PAT || process.env.GITHUB_TOKEN&lt;/code&gt; evaluates to &lt;code&gt;undefined&lt;/code&gt;. Template literals don't care. They'll happily stringify that into the header anyway:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Authorization: bearer undefined
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a syntactically valid HTTP header. GitHub's API has no way to know "undefined" isn't a real token — it just sees garbage credentials and responds with a 401, same as it would for any other malformed token.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why this one's easy to miss
&lt;/h4&gt;

&lt;p&gt;The failure mode looks exactly like an upstream GitHub problem. You get a 401, you check your token, you check your scopes, maybe you regenerate the PAT — none of that helps, because the actual bug is three layers upstream of the request that's failing.&lt;/p&gt;

&lt;p&gt;It also doesn't fail in CI or in prod, where the env var is almost always set. It only shows up for a contributor cloning the repo fresh and trying to run it locally without realizing a token is required. So the people most likely to hit it are exactly the people least equipped to debug it — first-time contributors, during setup, before they have any context on the codebase.&lt;/p&gt;

&lt;h4&gt;
  
  
  The fix
&lt;/h4&gt;

&lt;p&gt;Resolve the token once, validate it's actually there, and fail loudly before the request goes out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/github.ts — after&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MISSING_GITHUB_TOKEN_MESSAGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GitHub token is missing. Set GITHUB_PAT or GITHUB_TOKEN.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getGitHubToken&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GITHUB_PAT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MISSING_GITHUB_TOKEN_MESSAGE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getHeaders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;getGitHubToken&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same fallback behavior — &lt;code&gt;GITHUB_PAT&lt;/code&gt; first, &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; second — but now a missing token throws a clear &lt;code&gt;Error&lt;/code&gt; before &lt;code&gt;fetchWithRetry&lt;/code&gt; ever gets called. No network round trip. No GitHub 401 to misread. Just an immediate, readable message pointing at the actual problem.&lt;/p&gt;

&lt;p&gt;The tests cover all three cases now: &lt;code&gt;GITHUB_PAT&lt;/code&gt; present, falling back to &lt;code&gt;GITHUB_TOKEN&lt;/code&gt;, and neither set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;throws before fetching when no GitHub token is configured&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GITHUB_PAT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fetchGitHubContributions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;octocat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;rejects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toThrow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GitHub token is missing. Set GITHUB_PAT or GITHUB_TOKEN.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toHaveBeenCalled&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last assertion matters as much as the error message — it confirms the fetch never even fires once the token's missing, instead of trusting GitHub to reject it correctly.&lt;/p&gt;

&lt;h4&gt;
  
  
  What I'd flag for next time
&lt;/h4&gt;

&lt;p&gt;Any place a template literal builds an auth header from an env var deserves a second look. &lt;code&gt;${process.env.X}&lt;/code&gt; will never throw — it'll just smile and hand you the string &lt;code&gt;"undefined"&lt;/code&gt;. If the thing on the other end of that header is permissive enough to accept a malformed credential and reject it later, your error shows up in the wrong place, attributed to the wrong cause.&lt;/p&gt;

&lt;p&gt;If you're tracing a new codebase and you find a request that authenticates with an environment variable — check what happens when that variable isn't set. Don't assume it throws. Go read it. You might find your first PR sitting right there.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>opensource</category>
      <category>webdev</category>
      <category>testing</category>
    </item>
    <item>
      <title>How I Found a Silent API Bug in a 3,548-Star MCP Server</title>
      <dc:creator>Eshaan Agrawal</dc:creator>
      <pubDate>Mon, 29 Jun 2026 05:44:47 +0000</pubDate>
      <link>https://dev.to/eshaanagrawal/how-i-found-a-silent-api-bug-in-a-3548-star-mcp-server-1b55</link>
      <guid>https://dev.to/eshaanagrawal/how-i-found-a-silent-api-bug-in-a-3548-star-mcp-server-1b55</guid>
      <description>&lt;h1&gt;
  
  
  How I Found a Silent API Bug in a 3,548-Star MCP Server
&lt;/h1&gt;

&lt;p&gt;The API accepted your request. Returned HTTP 200. And then quietly did nothing.&lt;/p&gt;

&lt;p&gt;No error. No stack trace. Just an empty result where your Cypher query should have run.&lt;/p&gt;

&lt;p&gt;This is the story of how I found that bug in &lt;a href="https://github.com/CodeGraphContext/CodeGraphContext" rel="noopener noreferrer"&gt;CodeGraphContext&lt;/a&gt; — an MCP server that indexes local codebases into a graph database to give AI assistants real code context. 3,548 stars. Actively maintained. Used by developers building on top of Claude and other LLM toolchains.&lt;/p&gt;

&lt;p&gt;And it had a silent failure sitting in its public HTTP query endpoint.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is CodeGraphContext?
&lt;/h2&gt;

&lt;p&gt;Before the bug — quick context.&lt;/p&gt;

&lt;p&gt;CodeGraphContext is an MCP (Model Context Protocol) server. It lets you point it at a codebase, index it into a graph database (FalkorDB or Kuzu), and then query that graph using Cypher — the query language for graph databases. The idea is that AI assistants can use it to understand code structure, dependencies, and relationships at a depth that flat file reads don't give you.&lt;/p&gt;

&lt;p&gt;It exposes both an MCP interface and a standard HTTP API. The HTTP API is what's relevant here.&lt;/p&gt;




&lt;h2&gt;
  
  
  How I Found It
&lt;/h2&gt;

&lt;p&gt;I wasn't hunting for bugs. I was reading the source.&lt;/p&gt;

&lt;p&gt;When I contribute to a new repo, I start by tracing a single request end-to-end through the codebase. Pick an endpoint, follow it from the route definition into the handler, into any downstream calls, and back out. It's the fastest way to actually understand what a codebase does.&lt;/p&gt;

&lt;p&gt;I started with &lt;code&gt;/api/v1/query&lt;/code&gt; — the endpoint that lets you send a Cypher query over HTTP and get results back.&lt;/p&gt;

&lt;p&gt;The route lives in &lt;code&gt;src/codegraphcontext/api/router.py&lt;/code&gt;. Here's what I found at line 89:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handle_tool_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;execute_cypher_query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The route was calling &lt;code&gt;handle_tool_call&lt;/code&gt; with the tool name &lt;code&gt;execute_cypher_query&lt;/code&gt; and passing the query as &lt;code&gt;"query"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then I went to the handler. &lt;code&gt;src/codegraphcontext/tools/handlers/query_handlers.py&lt;/code&gt;, line 16:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;cypher_query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cypher_query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;cypher_query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cypher query cannot be empty.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The handler reads &lt;code&gt;cypher_query&lt;/code&gt;. Not &lt;code&gt;query&lt;/code&gt;. &lt;code&gt;cypher_query&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The route passes &lt;code&gt;query&lt;/code&gt;. The handler reads &lt;code&gt;cypher_query&lt;/code&gt;. They never meet.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;args.get("cypher_query")&lt;/code&gt; returns &lt;code&gt;None&lt;/code&gt;. The handler hits the empty check. Returns &lt;code&gt;{"error": "Cypher query cannot be empty."}&lt;/code&gt;. HTTP 200.&lt;/p&gt;

&lt;p&gt;A valid request, carrying a real Cypher query, silently failed — not because the query was wrong, but because the key name didn't match across an internal function call boundary.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Is Easy to Miss
&lt;/h2&gt;

&lt;p&gt;The failure mode is the problem.&lt;/p&gt;

&lt;p&gt;If this threw an exception, you'd see a 500. If it returned a 4xx, you'd know the API rejected your input. But it returned 200 with an error payload that looks like a validation error. The natural read is: "I sent a bad query." The actual problem is: the API layer and the tool handler were speaking different languages about what to call the same field.&lt;/p&gt;

&lt;p&gt;Here's the repro from the issue I filed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8000/api/v1/query &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"query":"MATCH (n) RETURN count(n) AS count","params":{}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Valid Cypher. Valid JSON. Valid HTTP request. But the tool handler sees no &lt;code&gt;cypher_query&lt;/code&gt; key — because the route called it &lt;code&gt;query&lt;/code&gt; — and returns empty.&lt;/p&gt;

&lt;p&gt;The request body looks correct at the API boundary. The failure happens after the internal translation. That's what makes it a confusing debugging path. You'd inspect the request, see a valid payload, and have no obvious reason to look at the key name being passed between the route and the handler.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;One line change in &lt;code&gt;router.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Before
&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handle_tool_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;execute_cypher_query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;# After
&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handle_tool_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;execute_cypher_query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cypher_query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Change the key from &lt;code&gt;"query"&lt;/code&gt; to &lt;code&gt;"cypher_query"&lt;/code&gt; so it matches what the handler expects.&lt;/p&gt;

&lt;p&gt;I kept the fix narrow — just aligning the key name, no refactoring, no schema changes. Then added a regression test in &lt;code&gt;tests/unit/api/test_query_router.py&lt;/code&gt; that verifies a POST to &lt;code&gt;/api/v1/query&lt;/code&gt; reaches &lt;code&gt;execute_cypher_query&lt;/code&gt; with the correct key. So this can't silently break again.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Added: The Regression Test
&lt;/h2&gt;

&lt;p&gt;This is the part I'd push anyone fixing a bug like this to do.&lt;/p&gt;

&lt;p&gt;The fix itself is obvious once you see it. What's not obvious is that without a test, the exact same mismatch can come back — someone refactors the handler to read a different key, or the route gets updated without checking what the handler expects, and you're back to square one.&lt;/p&gt;

&lt;p&gt;The test mocks &lt;code&gt;server.handle_tool_call&lt;/code&gt; and asserts that after a valid POST to &lt;code&gt;/api/v1/query&lt;/code&gt;, the mock is called with &lt;code&gt;"cypher_query"&lt;/code&gt; in the arguments dict. Small test, precise assertion, catches exactly this class of regression.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_query_route_passes_cypher_query_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mock_server&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/v1/query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MATCH (n) RETURN count(n)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;call_args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mock_server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handle_tool_call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call_args&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cypher_query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;call_args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Broader Pattern
&lt;/h2&gt;

&lt;p&gt;This is a specific instance of a bug pattern that shows up everywhere in layered systems: &lt;strong&gt;argument name drift&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You have a public interface (the HTTP route) that translates external input into an internal call. The public interface uses one name (&lt;code&gt;query&lt;/code&gt;). The internal interface expects another (&lt;code&gt;cypher_query&lt;/code&gt;). Both sides are internally consistent. The mismatch only exists at the translation boundary — and unless someone traces the full call path, it's invisible.&lt;/p&gt;

&lt;p&gt;It's especially common when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An internal tool or handler is defined separately from the route that calls it&lt;/li&gt;
&lt;li&gt;The field names are semantically similar but not identical (&lt;code&gt;query&lt;/code&gt; vs &lt;code&gt;cypher_query&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The failure mode is a validation error rather than an exception (so it looks like bad input, not a broken translation)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix is always the same: trace the call, align the names, add a test that pins the contract.&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;PR #1025: &lt;a href="https://github.com/CodeGraphContext/CodeGraphContext/pull/1025" rel="noopener noreferrer"&gt;fix(api): pass cypher query argument to query handler&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Issue #1023: &lt;a href="https://github.com/CodeGraphContext/CodeGraphContext/issues/1023" rel="noopener noreferrer"&gt;bug: HTTP query route passes wrong argument to execute_cypher_query&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/CodeGraphContext/CodeGraphContext" rel="noopener noreferrer"&gt;CodeGraphContext/CodeGraphContext&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;I also filed and opened PR #1026 on the same repo — the MCP SSE module was importing &lt;code&gt;mcp.server&lt;/code&gt; and &lt;code&gt;mcp.server.sse&lt;/code&gt; without declaring &lt;code&gt;mcp&lt;/code&gt; as a runtime dependency in &lt;code&gt;pyproject.toml&lt;/code&gt;. Clean installs could fail at import time with a &lt;code&gt;ModuleNotFoundError&lt;/code&gt; on a code path the project advertises. That one's a writeup for another day.&lt;/p&gt;

&lt;p&gt;If you're building on MCP or contributing to open source Python projects — read the source end-to-end before you start. One pass through a real request flow teaches you more about a codebase than reading the README three times.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This fix was part of my GSSoC 2026 contributions. I'm currently ranked #50 globally (S Tier, top 1% of 43,587 contributors) across 14 repos. Writing about the bugs that actually taught me something.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>mcp</category>
    </item>
  </channel>
</rss>
