<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.9.3">Jekyll</generator><link href="https://christopher.xyz/rss.xml" rel="self" type="application/atom+xml" /><link href="https://christopher.xyz/" rel="alternate" type="text/html" /><updated>2026-03-23T13:18:29+00:00</updated><id>https://christopher.xyz/rss.xml</id><title type="html">Christopher Dignam</title><subtitle>A blog about software development and technology.</subtitle><author><name>Christopher Dignam</name></author><entry><title type="html">Firebase Storage vs Cloud Storage Latency</title><link href="https://christopher.xyz/2026/03/23/firebase-storage-latency.html" rel="alternate" type="text/html" title="Firebase Storage vs Cloud Storage Latency" /><published>2026-03-23T00:00:00+00:00</published><updated>2026-03-23T00:00:00+00:00</updated><id>https://christopher.xyz/2026/03/23/firebase-storage-latency</id><content type="html" xml:base="https://christopher.xyz/2026/03/23/firebase-storage-latency.html">&lt;p&gt;Firebase Storage provides an easy way to save and retrieve files in Google Cloud Storage, without needing a server.&lt;/p&gt;

&lt;p&gt;The downside of Firebase Storage is the added &lt;em&gt;500ms&lt;/em&gt; of latency over using Google Cloud Storage directly.&lt;/p&gt;

&lt;h1 id=&quot;comparison&quot;&gt;Comparison&lt;/h1&gt;

&lt;p&gt;To compare the two services, I accessed the same file, multiple times via Firebase Storage and Cloud Storage.&lt;/p&gt;

&lt;p&gt;The DNS, TCP, TLS, and download speeds are similar, but the Time to First Byte (TTFB) averages 549ms for Firebase Storage versus 41ms for Google Cloud Storage.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Name&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;DNS&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;TCP&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;TLS&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;TTFB&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;Download&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Firebase Storage trial 1&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.041&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.017&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.036&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.477&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.285&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Firebase Storage trial 2&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.003&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.097&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.034&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.499&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.170&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Firebase Storage trial 3&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.003&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.027&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.042&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.672&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.185&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Firebase Storage average&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.016&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.047&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.037&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;&lt;strong&gt;0.549&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.213&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Cloud Storage trial 1&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.003&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.024&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.040&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.030&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.271&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Cloud Storage trial 2&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.005&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.105&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.034&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.032&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.170&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Cloud Storage trial 3&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.007&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.021&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.033&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.060&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.238&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Cloud Storage average&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.005&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.050&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.036&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;&lt;strong&gt;0.041&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.226&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h1 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h1&gt;

&lt;p&gt;To improve download performance, I enabled public read-only access to the bucket used by Firebase Storage.&lt;/p&gt;

&lt;p&gt;Instead of downloading objects from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;https://firebasestorage.googleapis.com/v0/b/{bucket_name}/0&lt;/code&gt;, I download objects from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;https://storage.googleapis.com/{bucket_name}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The objects are stored using random Firebase IDs, so it’s effectively impossible to guess the URLs, but once a user has the URL of a file, they’ll have access to that file forever.&lt;/p&gt;</content><author><name>Christopher Dignam</name></author><summary type="html">Firebase Storage provides an easy way to save and retrieve files in Google Cloud Storage, without needing a server.</summary></entry><entry><title type="html">Feedly API Tokens</title><link href="https://christopher.xyz/2026/02/22/feedly-api-tokens.html" rel="alternate" type="text/html" title="Feedly API Tokens" /><published>2026-02-22T00:00:00+00:00</published><updated>2026-02-22T00:00:00+00:00</updated><id>https://christopher.xyz/2026/02/22/feedly-api-tokens</id><content type="html" xml:base="https://christopher.xyz/2026/02/22/feedly-api-tokens.html">&lt;p&gt;For a personal project, I need API access to my Feedly account.&lt;/p&gt;

&lt;p&gt;Previously, with a paid Feedly account you could generate an API token for programmatic API access.&lt;/p&gt;

&lt;p&gt;Now, Feedly locks this feature behind an Enterprise account ($2,400/month, billed annually).&lt;/p&gt;

&lt;p&gt;So I needed to find another way to get API access.&lt;/p&gt;

&lt;h2 id=&quot;browser-dev-tools&quot;&gt;Browser Dev Tools&lt;/h2&gt;

&lt;p&gt;My first idea was to copy the Feedly &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Authorization&lt;/code&gt; header from my browser’s network tools. But this token expires after a few days.&lt;/p&gt;

&lt;h2 id=&quot;oauth-applications&quot;&gt;OAuth Applications&lt;/h2&gt;

&lt;p&gt;My second idea was to create a personal Feedly OAuth application, but this is locked behind approval from Feedly.&lt;/p&gt;

&lt;p&gt;My workaround was to leverage an existing OAuth application and copy the OAuth refresh token used by the application.&lt;/p&gt;

&lt;p&gt;NetNewsWire is an &lt;a href=&quot;https://github.com/Ranchero-Software/NetNewsWire&quot;&gt;open source&lt;/a&gt; RSS Reader for macOS with Feedly support.&lt;/p&gt;

&lt;p&gt;Since NetNewsWire uses OAuth to connect to the Feedly API, I can extract the OAuth credentials for my own use.&lt;/p&gt;

&lt;h3 id=&quot;apple-keychain&quot;&gt;Apple Keychain&lt;/h3&gt;

&lt;p&gt;After logging into my Feedly account with NetNewsWire, I found two &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cloud.feedly.com&lt;/code&gt; “Internet Password” entries in “Keychain Access.app”.&lt;/p&gt;

&lt;p&gt;One of these is the OAuth Access Token, and the second is the OAuth Refresh Token.&lt;/p&gt;

&lt;p&gt;Sadly, these two credentials aren’t enough to generate a new access token.&lt;/p&gt;

&lt;p&gt;We need the OAuth Client ID for the NetNewsWire Feedly OAuth application, and the OAuth Client Secret.&lt;/p&gt;

&lt;h3 id=&quot;charles-proxy&quot;&gt;Charles Proxy&lt;/h3&gt;

&lt;p&gt;Using a man-in-the-middle proxy, &lt;a href=&quot;https://www.charlesproxy.com&quot;&gt;Charles Proxy&lt;/a&gt;, I was able to intercept the web traffic between NetNewsWire and the Feedly OAuth API.&lt;/p&gt;

&lt;p&gt;From this, I was able to extract all of the OAuth credentials necessary to generate a new OAuth Access Token.&lt;/p&gt;

&lt;p&gt;I observed a request to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/v3/auth/token&lt;/code&gt; endpoint which contained the OAuth &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;client_id&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;client_secret&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;POST https://cloud.feedly.com/v3/auth/token
{
	&quot;redirect_uri&quot;: &quot;netnewswire:\/\/auth\/feedly&quot;,
	&quot;client_id&quot;: &quot;&amp;lt;NetNewsWire-client-id&amp;gt;&quot;,
	&quot;code&quot;: &quot;&amp;lt;fac_code&amp;gt;&quot;,
	&quot;client_secret&quot;: &quot;&amp;lt;NetNewsWire-client-secret&amp;gt;&quot;,
	&quot;scope&quot;: &quot;https:\/\/cloud.feedly.com\/subscriptions&quot;,
	&quot;grant_type&quot;: &quot;authorization_code&quot;
}

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The response contained an OAuth &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;refresh_token&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;access_token&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&amp;lt;my-feedly-user-id&amp;gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;provider&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Google&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;is_new_account&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;plan&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;proPlus&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;scope&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;https://cloud.feedly.com/subscriptions&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;refresh_token&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&amp;lt;refresh:token&amp;gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;access_token&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&amp;lt;JWE.token.here&amp;gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;token_type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;bearer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;expires_in&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;604800&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now with all of the required information, I could call the Feedly API myself and generate a new OAuth access token.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;curl &lt;span class=&quot;nt&quot;&gt;-X&lt;/span&gt; POST https://cloud.feedly.com/v3/auth/token &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-H&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Content-Type: application/x-www-form-urlencoded&quot;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;grant_type=refresh_token&quot;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;refresh_token=&amp;lt;feedly-refresh-token&amp;gt;&quot;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;client_id=&amp;lt;feedly-client-id&amp;gt;&quot;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;client_secret=&amp;lt;feedly-client-secret&amp;gt;&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;</content><author><name>Christopher Dignam</name></author><summary type="html">For a personal project, I need API access to my Feedly account.</summary></entry><entry><title type="html">Improving Git Repository Performance</title><link href="https://christopher.xyz/2026/01/25/faster-git-repo-performance.html" rel="alternate" type="text/html" title="Improving Git Repository Performance" /><published>2026-01-25T00:00:00+00:00</published><updated>2026-01-25T00:00:00+00:00</updated><id>https://christopher.xyz/2026/01/25/faster-git-repo-performance</id><content type="html" xml:base="https://christopher.xyz/2026/01/25/faster-git-repo-performance.html">&lt;p&gt;For large repositories, the easiest way to improve performance is to enable &lt;a href=&quot;https://git-scm.com/docs/git-config#Documentation/git-config.txt-featuremanyFiles&quot;&gt;feature.manyFiles&lt;/a&gt;:&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; feature.manyFiles &lt;span class=&quot;nb&quot;&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Enabling this configuration will enable a newer index format and make &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git status&lt;/code&gt; faster.&lt;/p&gt;

&lt;p&gt;With &lt;a href=&quot;https://git-scm.com/docs/git-config#Documentation/git-config.txt-corefsmonitor&quot;&gt;core.fsmonitor&lt;/a&gt;, Git will use FSEvents to monitor your repository for changes in a daemon.&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; core.fsmonitor &lt;span class=&quot;nb&quot;&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This will improve &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git status&lt;/code&gt; performance too.&lt;/p&gt;

&lt;p&gt;Some &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git log&lt;/code&gt; commands can be sped up with &lt;a href=&quot;https://git-scm.com/docs/git-fetch#Documentation/git-fetch.txt-fetchwriteCommitGraph&quot;&gt;fetch.writeCommitGraph&lt;/a&gt;, but I didn’t notice a huge change with this.&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; fetch.writeCommitGraph &lt;span class=&quot;nb&quot;&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I don’t notice &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git checkout&lt;/code&gt; being slow, but &lt;a href=&quot;https://git-scm.com/docs/git-checkout#Documentation/git-checkout.txt-checkoutworkers&quot;&gt;checkout.workers&lt;/a&gt; is supposed to help on SSDs:&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; checkout.workers 0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h1 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h1&gt;

&lt;p&gt;Enable these settings to improve repository performance.&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; feature.manyFiles &lt;span class=&quot;nb&quot;&gt;true
&lt;/span&gt;git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; core.fsmonitor &lt;span class=&quot;nb&quot;&gt;true
&lt;/span&gt;git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; fetch.writeCommitGraph &lt;span class=&quot;nb&quot;&gt;true
&lt;/span&gt;git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; checkout.workers 0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;</content><author><name>Christopher Dignam</name></author><summary type="html">For large repositories, the easiest way to improve performance is to enable feature.manyFiles:</summary></entry><entry><title type="html">FNM (Fast Node Manager)</title><link href="https://christopher.xyz/2026/01/23/fnm.html" rel="alternate" type="text/html" title="FNM (Fast Node Manager)" /><published>2026-01-23T00:00:00+00:00</published><updated>2026-01-23T00:00:00+00:00</updated><id>https://christopher.xyz/2026/01/23/fnm</id><content type="html" xml:base="https://christopher.xyz/2026/01/23/fnm.html">&lt;p&gt;&lt;a href=&quot;https://github.com/Schniz/fnm&quot;&gt;FNM&lt;/a&gt; manages Node versions for your different projects.&lt;/p&gt;

&lt;p&gt;FNM provides more features and is significantly faster than &lt;a href=&quot;https://github.com/nvm-sh/nvm&quot;&gt;NVM&lt;/a&gt;, the de facto tool for managing Node versions.&lt;/p&gt;

&lt;p&gt;Let’s compare the features and performance.&lt;/p&gt;

&lt;h1 id=&quot;shell-integration&quot;&gt;Shell Integration&lt;/h1&gt;

&lt;p&gt;When switching directories, FNM will automatically enable the required Node version based on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.nvmrc&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.node-version&lt;/code&gt; files in the directory.&lt;/p&gt;

&lt;p&gt;If the Node version is not installed already, FNM will prompt you to install it.&lt;/p&gt;

&lt;p&gt;NVM doesn’t provide this functionality out-of-the-box and &lt;a href=&quot;https://github.com/nvm-sh/nvm/blob/ec8906b284d000b414da78855be90d7ae8152b77/README.md#deeper-shell-integration&quot;&gt;recommends using other tools&lt;/a&gt;.&lt;/p&gt;

&lt;h1 id=&quot;performance&quot;&gt;Performance&lt;/h1&gt;

&lt;p&gt;FNM is fast.&lt;/p&gt;

&lt;p&gt;Calling &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fnm use&lt;/code&gt; takes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4ms&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The equivalent &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvm use&lt;/code&gt; takes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;450ms&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;FNM’s great performance means the shell integration won’t slow you down, unlike NVM.&lt;/p&gt;

&lt;h1 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h1&gt;

&lt;p&gt;FNM is better than NVM for managing Node versions.&lt;/p&gt;</content><author><name>Christopher Dignam</name></author><summary type="html">FNM manages Node versions for your different projects.</summary></entry><entry><title type="html">How to generate a Firebase ID Token</title><link href="https://christopher.xyz/2025/05/18/firebase-auth-tokens.html" rel="alternate" type="text/html" title="How to generate a Firebase ID Token" /><published>2025-05-18T00:00:00+00:00</published><updated>2025-05-18T00:00:00+00:00</updated><id>https://christopher.xyz/2025/05/18/firebase-auth-tokens</id><content type="html" xml:base="https://christopher.xyz/2025/05/18/firebase-auth-tokens.html">&lt;p&gt;Firebase Functions has &lt;a href=&quot;https://firebase.google.com/docs/functions/callable?gen=2nd&quot;&gt;a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;https.onCall&lt;/code&gt; trigger&lt;/a&gt; that supports calling the Function from your Firebase client.&lt;/p&gt;

&lt;p&gt;For the Mobile and Web Firebase SDKs, authentication is handled for you. But if you want to call these Cloud Functions manually, you need a Firebase ID Token.&lt;/p&gt;

&lt;h1 id=&quot;generating-an-id-token&quot;&gt;Generating an ID Token&lt;/h1&gt;

&lt;p&gt;Creating an ID Token is a two step process. We first create a Custom Token for a user, and then exchange that token for an ID Token.&lt;/p&gt;

&lt;h2 id=&quot;create-a-custom-token&quot;&gt;Create a Custom Token&lt;/h2&gt;

&lt;p&gt;Using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;firebase_admin&lt;/code&gt; SDK, we can generate a Custom Token for a given Firebase Auth &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uid&lt;/code&gt; (User ID).&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;firebase_admin&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;initialize_app&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;auth&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;initialize_app&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;uid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;T29cMsbYVyNTNQhnkIApXIBDWB73&quot;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;custom_token&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;auth&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;create_custom_token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;uid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;custom_token&lt;/code&gt; doesn’t allow us to authenticate with Firestore. Instead, we must exchange this token for our ID Token using Google Cloud APIs.&lt;/p&gt;

&lt;h2 id=&quot;exchanging-a-custom-token-for-id-token&quot;&gt;Exchanging a Custom Token for ID Token&lt;/h2&gt;

&lt;p&gt;First, you’ll need a Google Cloud API Key which can be &lt;a href=&quot;https://console.cloud.google.com/apis/credentials&quot;&gt;generated on the Google Cloud Console&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This API Key only needs Identity Toolkit access, so edit your key’s API restrictions to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Identity Toolkit API&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now we can use our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;custom_token&lt;/code&gt; from the previous step, along with our Google Cloud API key to &lt;a href=&quot;https://cloud.google.com/identity-platform/docs/reference/rest/v1/accounts/signInWithCustomToken&quot;&gt;call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;signInWithCustomToken&lt;/code&gt;&lt;/a&gt; and retrieve an ID Token:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;CUSTOM_TOKEN&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;custom_token-from-previous-step&quot;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;API_KEY&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;your-google-cloud-api-token&quot;&lt;/span&gt;
curl &lt;span class=&quot;nt&quot;&gt;-X&lt;/span&gt; POST &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;s2&quot;&gt;&quot;https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$API_KEY&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-H&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Content-Type: application/json&apos;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;{
    {&quot;token&quot;: &quot;$CUSTOM_TOKEN&quot;, &quot;returnSecureToken&quot;: true}
  }&apos;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;kind&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;identitytoolkit#VerifyCustomTokenResponse&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;idToken&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&amp;lt;id.token.jwt&amp;gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;refreshToken&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&amp;lt;refresh-token&amp;gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;expiresIn&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;3600&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;isNewUser&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;idToken&lt;/code&gt; in this response will allow us to authenticate with Firebase Functions.&lt;/p&gt;

&lt;h1 id=&quot;calling-an-authenticated-firebase-function&quot;&gt;Calling an Authenticated Firebase Function&lt;/h1&gt;

&lt;p&gt;With our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;idToken&lt;/code&gt;, we can call our Firebase Function, using &lt;a href=&quot;https://firebase.google.com/docs/functions/callable-reference&quot;&gt;the protocol specification for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;https.onCall&lt;/code&gt;&lt;/a&gt; as a reference.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;ID_TOKEN&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&amp;lt;id.token.jwt&amp;gt;&quot;&lt;/span&gt;
curl &lt;span class=&quot;nt&quot;&gt;-X&lt;/span&gt; POST &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;s1&quot;&gt;&apos;https://us-central1-firebase-project-name.cloudfunctions.net/myFunctionName&apos;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-H&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Authentication: Bearer &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$ID_TOKEN&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-H&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Content-Type: application/json&apos;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;# firebase function arguments are passed in the `data` param:&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;{
    &quot;data&quot;: {}
  }&apos;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;</content><author><name>Christopher Dignam</name></author><summary type="html">Firebase Functions has a https.onCall trigger that supports calling the Function from your Firebase client.</summary></entry><entry><title type="html">Prototype Pollution in Javascript</title><link href="https://christopher.xyz/2024/05/24/prototype-pollution.html" rel="alternate" type="text/html" title="Prototype Pollution in Javascript" /><published>2024-05-24T00:00:00+00:00</published><updated>2024-05-24T00:00:00+00:00</updated><id>https://christopher.xyz/2024/05/24/prototype-pollution</id><content type="html" xml:base="https://christopher.xyz/2024/05/24/prototype-pollution.html">&lt;p&gt;At work a small percentage of requests were failing with bizzare error messages. A quick fix was to roll back our deployment, but it took a year and a half to find a solution to the underlying bug.&lt;/p&gt;

&lt;p&gt;The root cause: prototype pollution&lt;/p&gt;

&lt;h2 id=&quot;prototype-pollution&quot;&gt;Prototype pollution&lt;/h2&gt;

&lt;p&gt;By setting a property on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Object.prototype&lt;/code&gt;, we modify the prototype of all objects in Javascript. This is prototype pollution.&lt;/p&gt;

&lt;p&gt;In this example, we set the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hello&lt;/code&gt; property on our Object prototype. Now all objects in our Javascript process will have the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hello&lt;/code&gt; property.&lt;/p&gt;

&lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;foo&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;hello&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;foo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// false&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;Object&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;prototype&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;hello&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;world&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;hello&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;foo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// true&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;hello&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{})&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You normally wouldn’t modify &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Object.prototype&lt;/code&gt;, but you can do so accidentally with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;__proto__&lt;/code&gt; property.&lt;/p&gt;

&lt;p&gt;We can rewrite the previous example to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;__proto__&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;foo&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;hello&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;foo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// false&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;foo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;__proto__&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;hello&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;world&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;hello&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;foo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// true&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;hello&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{})&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;suspicious-code&quot;&gt;Suspicious Code&lt;/h2&gt;

&lt;p&gt;This is a simplified recreation of the production code.&lt;/p&gt;

&lt;p&gt;Because the bug is triggered by user-generated data, the bug was able to exist in the codebase for over a year and half before triggering an error.&lt;/p&gt;

&lt;p&gt;This bug was only discovered after many of my previous attempts came up empty.&lt;/p&gt;

&lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;events&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;properties&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
      &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;user_id&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;// the problematic user-provided attribute&lt;/span&gt;
      &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;__proto__&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;navigate&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;properties&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;user_id&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;countsByProperty&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;events&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;property&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;of&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;properties&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// when &quot;property&quot; is &quot;__proto__&quot;, we retrieve the prototype of countsByProperty.&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;entry&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;countsByProperty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;property&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;entry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;entry&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// &quot;entry&quot; is the Object prototype.&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;entry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// set the &quot;count&quot; property on &quot;Object.prototype&quot;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// all objects will have an inherited property of &quot;count&quot;.&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{})&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This on it’s own isn’t enough to cause damage. By setting &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;count&lt;/code&gt; on the prototype, we set an “inherited property” for future objects. Iteration done correctly ignores them.&lt;/p&gt;

&lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;__proto__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;of&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// no output&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In our case, we used &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lodash.omitBy&lt;/code&gt; to removed undefined properties from an object.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lodash.omitBy&lt;/code&gt; iterates over both “own properties” and “inherited properties”, returning a new object with all properties set as “own properties”.&lt;/p&gt;

&lt;p&gt;The previously invisible &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;count&lt;/code&gt; property, now became an iterable property in the new object created by Lodash. This new attribute triggered errors in our service, causing requests to fail.&lt;/p&gt;

&lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;omitBy&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;lodash/omitBy&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;__proto__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// true&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;Object&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;keys&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 0&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;omitBy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;Object&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;keys&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Once the object prototype was modified in our running service, the only fix was to restart.&lt;/p&gt;

&lt;p&gt;It took three years from when the bug was introduced to when it was fixed in our system.&lt;/p&gt;

&lt;h3 id=&quot;concise-example&quot;&gt;Concise Example&lt;/h3&gt;

&lt;p&gt;Here’s a minimal version of the prototype pollution code.&lt;/p&gt;

&lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;countsByProperty&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;property&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;__proto__&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;entry&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;countsByProperty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;property&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// entry is equal to &quot;Object.prototype&quot;.&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;entry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// all future objects will have an inherited property of &quot;count&quot;.&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{})&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;solutions&quot;&gt;Solutions&lt;/h2&gt;

&lt;p&gt;Removing the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;__proto__&lt;/code&gt; attribute from objects in Javascript is the most robust solution.&lt;/p&gt;

&lt;h3 id=&quot;node&quot;&gt;Node&lt;/h3&gt;

&lt;p&gt;We can remove &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;__proto__&lt;/code&gt; via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node --disable-proto=delete&lt;/code&gt;.&lt;/p&gt;

&lt;h3 id=&quot;deno&quot;&gt;Deno&lt;/h3&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;__proto__&lt;/code&gt; &lt;a href=&quot;https://github.com/denoland/deno/blob/8ea9370c55b01cc569289002b798886933f6a905/runtime/js/99_main.js#L850-L854&quot;&gt;is removed by default&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;browsers&quot;&gt;Browsers&lt;/h3&gt;

&lt;p&gt;There’s no solution to remove &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;__proto__&lt;/code&gt;. Use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Map&lt;/code&gt; instead of plain objects for hash maps.&lt;/p&gt;

&lt;p&gt;Instead of an object:&lt;/p&gt;

&lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;properties&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;__proto__&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// plain object vulnerable to prototype pollution in browser.&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;propertyCounts&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;properties&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;property&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;property&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;propertyCounts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;propertyCounts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;property&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// vulnerable to prototype pollution.&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;propertyCounts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;property&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Use a Map:&lt;/p&gt;

&lt;div class=&quot;language-ts highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;properties&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;__proto__&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// Map() is safe to use with user-provided keys.&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;propertyCounts&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;properties&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;property&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;propertyCounts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;has&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;property&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;propertyCounts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;property&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;propertyCounts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;property&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;</content><author><name>Christopher Dignam</name></author><summary type="html">At work a small percentage of requests were failing with bizzare error messages. A quick fix was to roll back our deployment, but it took a year and a half to find a solution to the underlying bug.</summary></entry><entry><title type="html">Fast Image Resizing</title><link href="https://christopher.xyz/2024/03/17/image-resizing-comparision.html" rel="alternate" type="text/html" title="Fast Image Resizing" /><published>2024-03-17T00:00:00+00:00</published><updated>2024-03-17T00:00:00+00:00</updated><id>https://christopher.xyz/2024/03/17/image-resizing-comparision</id><content type="html" xml:base="https://christopher.xyz/2024/03/17/image-resizing-comparision.html">&lt;p&gt;On &lt;a href=&quot;https://recipeyak.com&quot;&gt;Recipeyak&lt;/a&gt;, we resize user-uploaded images to improve page performance (see &lt;a href=&quot;/2024/02/10/image-optimization-platforms.html&quot;&gt;“Image Optimization”&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;We use Twicpic because the free tier is large, but I was curious how fast Twicpic was compared to the gold standard, Imgix.&lt;/p&gt;

&lt;h2 id=&quot;resizing-http-apis&quot;&gt;resizing HTTP APIs&lt;/h2&gt;

&lt;p&gt;Twicpic and Imgix provide image resizing HTTP APIs. The first request for an &lt;a href=&quot;/assets/images/IMG_9631.HEIC&quot;&gt;image&lt;/a&gt; will trigger the service to resize and cache the image. The following requests will pull from the CDN.&lt;/p&gt;

&lt;p&gt;In this comparison, Imgix took 1.5 seconds to resize an image, while TwicPics took 2.1 seconds.&lt;/p&gt;

&lt;p&gt;The follow up requests from the CDN are significantly faster.&lt;/p&gt;

&lt;h3 id=&quot;imgix&quot;&gt;Imgix&lt;/h3&gt;

&lt;p&gt;https://recipeyak.imgix.net/IMG_9631.HEIC?w=100&amp;amp;h=100&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;trial&lt;/th&gt;
      &lt;th&gt;duration (seconds)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;1.47&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2&lt;/td&gt;
      &lt;td&gt;0.14 (cached)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;3&lt;/td&gt;
      &lt;td&gt;0.11 (cached)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;4&lt;/td&gt;
      &lt;td&gt;0.10 (cached)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;5&lt;/td&gt;
      &lt;td&gt;0.08 (cached)&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;twicpic&quot;&gt;Twicpic&lt;/h3&gt;

&lt;p&gt;https://recipeyak-images.twic.pics/IMG_9631.HEIC?twic=v1/contain=100x100&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;trial&lt;/th&gt;
      &lt;th&gt;duration (seconds)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;2.09&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2&lt;/td&gt;
      &lt;td&gt;0.14 (cached)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;3&lt;/td&gt;
      &lt;td&gt;0.08 (cached)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;4&lt;/td&gt;
      &lt;td&gt;0.09 (cached)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;5&lt;/td&gt;
      &lt;td&gt;0.07 (cached)&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&quot;resizing-clis&quot;&gt;resizing CLIs&lt;/h2&gt;

&lt;p&gt;We can resize images ourselves with libaries like ImageMagick, sips, and libvips.&lt;/p&gt;

&lt;h3 id=&quot;imagemagick&quot;&gt;ImageMagick&lt;/h3&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;convert -define jpeg:size=100x100 IMG_9631.HEIC -auto-orient \
 -thumbnail 100x100 imagemagick.jpeg
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;trial&lt;/th&gt;
      &lt;th&gt;duration (seconds)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;0.46&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2&lt;/td&gt;
      &lt;td&gt;0.28&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;3&lt;/td&gt;
      &lt;td&gt;0.30&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;4&lt;/td&gt;
      &lt;td&gt;0.28&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;5&lt;/td&gt;
      &lt;td&gt;0.27&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;sips&quot;&gt;sips&lt;/h3&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;sips --resampleWidth 100 IMG_9631.HEIC --out sip.heic
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;trial&lt;/th&gt;
      &lt;th&gt;duration (seconds)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;0.38&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2&lt;/td&gt;
      &lt;td&gt;0.35&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;3&lt;/td&gt;
      &lt;td&gt;0.36&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;4&lt;/td&gt;
      &lt;td&gt;0.34&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;5&lt;/td&gt;
      &lt;td&gt;0.36&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;libvips&quot;&gt;libvips&lt;/h3&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;vipsthumbnail IMG_9631.HEIC --size 100x100
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;trial&lt;/th&gt;
      &lt;th&gt;duration (seconds)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;0.10&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2&lt;/td&gt;
      &lt;td&gt;0.10&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;3&lt;/td&gt;
      &lt;td&gt;0.10&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;4&lt;/td&gt;
      &lt;td&gt;0.10&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;5&lt;/td&gt;
      &lt;td&gt;0.10&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;Twicpic is slow, but Imgix isn’t much better. I think generating thumbnails ahead of time would deliver the best user experience.&lt;/p&gt;

&lt;h2 id=&quot;links&quot;&gt;Links&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://stackoverflow.com/a/57058078/&quot;&gt;StackOverflow post benchmarking image processing libraries&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/fawick/speedtest-resize&quot;&gt;github.com/fawick/speedtest-resize&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><author><name>Christopher Dignam</name></author><summary type="html">On Recipeyak, we resize user-uploaded images to improve page performance (see “Image Optimization”).</summary></entry><entry><title type="html">Image Optimization</title><link href="https://christopher.xyz/2024/02/10/image-optimization-platforms.html" rel="alternate" type="text/html" title="Image Optimization" /><published>2024-02-10T00:00:00+00:00</published><updated>2024-02-10T00:00:00+00:00</updated><id>https://christopher.xyz/2024/02/10/image-optimization-platforms</id><content type="html" xml:base="https://christopher.xyz/2024/02/10/image-optimization-platforms.html">&lt;p&gt;On Recipeyak, users upload images for recipes and we display them throughout the app.&lt;/p&gt;

&lt;p&gt;For pages with many images, we don’t want to show full size images if they only display at 200 pixels wide. Browsers are slow to render many large images.&lt;/p&gt;

&lt;p&gt;The solution is to use smaller images.&lt;/p&gt;

&lt;h2 id=&quot;generating-smaller-images&quot;&gt;Generating smaller images&lt;/h2&gt;

&lt;p&gt;We could resize our images into a set of predefined sizes. But we’d need to figure out those sizes ahead of time. Also, if we need other sizes in the future, we’d need to backfill older uploads.&lt;/p&gt;

&lt;p&gt;A simpler solution is to use an on demand image optimization service, like &lt;a href=&quot;https://imgix.com&quot;&gt;Imgix&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;With Imgix, we can request any image size we want at request time via a query parameter. So if we wanted our image to be 100x100px, we can rewrite our URL with this query parameter:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;https://example.com/image_4032.jpeg?h=100&amp;amp;w=100
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This works great for all the various image sizes we display throughout the app.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;size (px)&lt;/th&gt;
      &lt;th&gt;page&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;25x25&lt;/td&gt;
      &lt;td&gt;calendar&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;40x40&lt;/td&gt;
      &lt;td&gt;nav search&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;48x48&lt;/td&gt;
      &lt;td&gt;home (schedule, recently viewed, recently added)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;60x60&lt;/td&gt;
      &lt;td&gt;schedule modal&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;100x100&lt;/td&gt;
      &lt;td&gt;comment image thumbnail&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;244x180&lt;/td&gt;
      &lt;td&gt;list recipes&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;620x413&lt;/td&gt;
      &lt;td&gt;recipe&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;~1200x900&lt;/td&gt;
      &lt;td&gt;gallery (full page image)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;1200x910&lt;/td&gt;
      &lt;td&gt;Open Graph&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;In reality we only use a few image sizes in the app, but Imgix makes it easy to experiment and add new variants.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;1200 pixels wide&lt;/li&gt;
  &lt;li&gt;200 pixels wide&lt;/li&gt;
  &lt;li&gt;1200x910 pixels&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;improving-caching&quot;&gt;Improving caching&lt;/h2&gt;

&lt;p&gt;Imgix takes a noticeable amount of time to generate an image variant. With their caching this is less of an issue, but I’ve found the cache expires too quickly. So I placed Bunny.net in front and enabled PermaCache, to ensure that image variants are cached forever.&lt;/p&gt;

&lt;h2 id=&quot;finding-a-cheap-solution&quot;&gt;Finding a cheap solution&lt;/h2&gt;

&lt;p&gt;Imgix has excellent docs and works well, but the free tier has a relatively small limit of 1000 source images.&lt;/p&gt;

&lt;p&gt;For Recipeyak we have over 1,200 images now and recently hit this free tier limit. With the basic plan of $75/month way outside our cheap budget for Recipeyak, we needed an alternative.&lt;/p&gt;

&lt;p&gt;Imagekit.io was the first contender. With drop-in support for Imgix’s API and a generous free tier, it worked great. Until we uploaded a JPEG 2000 (.jp2) image, which Imagekit doesn’t support.&lt;/p&gt;

&lt;p&gt;Now we’re using Twicpic, which doesn’t have as nice of an API as Imgix, but has a decent free tier and supports all of our image formats.&lt;/p&gt;</content><author><name>Christopher Dignam</name></author><summary type="html">On Recipeyak, users upload images for recipes and we display them throughout the app.</summary></entry><entry><title type="html">Image compression in messaging apps</title><link href="https://christopher.xyz/2023/09/26/image-sizes.html" rel="alternate" type="text/html" title="Image compression in messaging apps" /><published>2023-09-26T00:00:00+00:00</published><updated>2024-06-13T00:00:00+00:00</updated><id>https://christopher.xyz/2023/09/26/image-sizes</id><content type="html" xml:base="https://christopher.xyz/2023/09/26/image-sizes.html">&lt;p&gt;Images sent over messaging apps are compressed before being delivered. This makes messages faster to send, but comes at a cost of visual fidelity.&lt;/p&gt;

&lt;h2 id=&quot;images&quot;&gt;Images&lt;/h2&gt;

&lt;p&gt;iMessage has the highest image quality, with HEIF encoding at size similar to Discord’s JPEG encoding.&lt;/p&gt;

&lt;p&gt;One caveat with iMessage is that if you share a 48MP RAW image, iMessage will downsize the image to 12MP before sharing.&lt;/p&gt;

&lt;!-- using IMG_9631.HEIC --&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt;Original&lt;/th&gt;
      &lt;th&gt;iMessage&lt;/th&gt;
      &lt;th&gt;Discord&lt;/th&gt;
      &lt;th&gt;WhatsApp (HD)&lt;/th&gt;
      &lt;th&gt;WhatsApp&lt;/th&gt;
      &lt;th&gt;Signal&lt;/th&gt;
      &lt;th&gt;Instagram&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Resolution (MP)&lt;/td&gt;
      &lt;td&gt;12&lt;/td&gt;
      &lt;td&gt;12&lt;/td&gt;
      &lt;td&gt;12&lt;/td&gt;
      &lt;td&gt;12&lt;/td&gt;
      &lt;td&gt;2&lt;/td&gt;
      &lt;td&gt;2&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Encoding&lt;/td&gt;
      &lt;td&gt;HEIF&lt;/td&gt;
      &lt;td&gt;HEIF&lt;/td&gt;
      &lt;td&gt;JPEG&lt;/td&gt;
      &lt;td&gt;JPEG&lt;/td&gt;
      &lt;td&gt;JPEG&lt;/td&gt;
      &lt;td&gt;JPEG&lt;/td&gt;
      &lt;td&gt;JPEG&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Size (MB)&lt;/td&gt;
      &lt;td&gt;2.4&lt;/td&gt;
      &lt;td&gt;2.4&lt;/td&gt;
      &lt;td&gt;6&lt;/td&gt;
      &lt;td&gt;1.9&lt;/td&gt;
      &lt;td&gt;0.4&lt;/td&gt;
      &lt;td&gt;0.7&lt;/td&gt;
      &lt;td&gt;0.5&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&quot;videos&quot;&gt;Videos&lt;/h2&gt;

&lt;p&gt;iMessage has the highest video quality, using HEVC encoding at 720p video, but the framerate is dropped to 30fps. Any panning shot becomes noticeably choppy with the loss of framerate.&lt;/p&gt;

&lt;p&gt;Videos shared via WhatsApp and Instagram are almost unusable because of the compression.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt;Original&lt;/th&gt;
      &lt;th&gt;iMessage&lt;/th&gt;
      &lt;th&gt;Discord&lt;/th&gt;
      &lt;th&gt;WhatsApp (HD)&lt;/th&gt;
      &lt;th&gt;WhatsApp&lt;/th&gt;
      &lt;th&gt;Signal&lt;/th&gt;
      &lt;th&gt;Instagram&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Resolution (height)&lt;/td&gt;
      &lt;td&gt;2160&lt;/td&gt;
      &lt;td&gt;720&lt;/td&gt;
      &lt;td&gt;360&lt;/td&gt;
      &lt;td&gt;720&lt;/td&gt;
      &lt;td&gt;480&lt;/td&gt;
      &lt;td&gt;360&lt;/td&gt;
      &lt;td&gt;360&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Framerate (fps)&lt;/td&gt;
      &lt;td&gt;60&lt;/td&gt;
      &lt;td&gt;30&lt;/td&gt;
      &lt;td&gt;60&lt;/td&gt;
      &lt;td&gt;60&lt;/td&gt;
      &lt;td&gt;60&lt;/td&gt;
      &lt;td&gt;59.86&lt;/td&gt;
      &lt;td&gt;29.77&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Encoding&lt;/td&gt;
      &lt;td&gt;HEVC&lt;/td&gt;
      &lt;td&gt;HEVC&lt;/td&gt;
      &lt;td&gt;H.264&lt;/td&gt;
      &lt;td&gt;H.264&lt;/td&gt;
      &lt;td&gt;H.264&lt;/td&gt;
      &lt;td&gt;H.264&lt;/td&gt;
      &lt;td&gt;H.264&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Size (MB)&lt;/td&gt;
      &lt;td&gt;52.9&lt;/td&gt;
      &lt;td&gt;2.3&lt;/td&gt;
      &lt;td&gt;3.8&lt;/td&gt;
      &lt;td&gt;2.7&lt;/td&gt;
      &lt;td&gt;1.4&lt;/td&gt;
      &lt;td&gt;3.8&lt;/td&gt;
      &lt;td&gt;0.6&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;If you care about image or video quality, don’t use messaging apps for sharing. Use methods that preserve full quality, like AirDrop or an &lt;a href=&quot;https://support.apple.com/guide/icloud/share-photos-and-videos-mm93a9b98683/icloud&quot;&gt;iCloud Link&lt;/a&gt;.&lt;/p&gt;</content><author><name>Christopher Dignam</name></author><summary type="html">Images sent over messaging apps are compressed before being delivered. This makes messages faster to send, but comes at a cost of visual fidelity.</summary></entry><entry><title type="html">Disable commits to the main branch</title><link href="https://christopher.xyz/2023/04/30/preventing-commits-to-main.html" rel="alternate" type="text/html" title="Disable commits to the main branch" /><published>2023-04-30T00:00:00+00:00</published><updated>2023-04-30T00:00:00+00:00</updated><id>https://christopher.xyz/2023/04/30/preventing-commits-to-main</id><content type="html" xml:base="https://christopher.xyz/2023/04/30/preventing-commits-to-main.html">&lt;p&gt;I unintentionally create commits on my &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main&lt;/code&gt; branch all the time. When this happens, I need to undo my changes, or delete my local main branch and checkout a fresh copy from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;origin&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With a Git hook, we can prevent commits being made to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main&lt;/code&gt; branch.&lt;/p&gt;

&lt;p&gt;I found an &lt;a href=&quot;https://stackoverflow.com/a/40465455/&quot;&gt;example of this on Stack Overflow&lt;/a&gt;, which works, but I wanted a solution that I could configure with multiple branch names.&lt;/p&gt;

&lt;p&gt;I wrote the pre-commit hook in Python, which I find easier to work with than Bash.&lt;/p&gt;

&lt;p&gt;Use the following steps:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; ~/.git-hooks
git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; core.hooksPath ~/.git-hooks/
&lt;span class=&quot;nb&quot;&gt;touch&lt;/span&gt; ~/.git-hooks/pre-commit
&lt;span class=&quot;nb&quot;&gt;chmod&lt;/span&gt; +x ~/.git-hooks/pre-commit
&lt;span class=&quot;c&quot;&gt;# copy the following python code into ~/.git-hooks/pre-commit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;#!/usr/bin/env python3
&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;subprocess&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;sys&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;os&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;PROTECTED_BRANCHES&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;master&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;main&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;DISABLE_KEY_PATH&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;protected-main.disabled&quot;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;disabled&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;bool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;subprocess&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;git&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;config&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;--get&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;DISABLE_KEY_PATH&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;capture_output&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;stdout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;decode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;strip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;disabled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;branch&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;subprocess&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;git&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;rev-parse&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;--abbrev-ref&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;HEAD&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;capture_output&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;check&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;stdout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;decode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;strip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;branch&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PROTECTED_BRANCHES&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;You can&apos;t commit directly to the &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;branch&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; branch&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sys&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;stderr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Disable with `git config --add &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DISABLE_KEY_PATH&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; 1 &quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sys&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;stderr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;nb&quot;&gt;exit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;


&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;__name__&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;__main__&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;</content><author><name>Christopher Dignam</name></author><summary type="html">I unintentionally create commits on my main branch all the time. When this happens, I need to undo my changes, or delete my local main branch and checkout a fresh copy from origin.</summary></entry></feed>