<?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>Forem: Tianhao Zhou</title>
    <description>The latest articles on Forem by Tianhao Zhou (@htuohz).</description>
    <link>https://forem.com/htuohz</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F767758%2F81dc931e-52e7-4b77-8dfd-74b5b431333d.jpeg</url>
      <title>Forem: Tianhao Zhou</title>
      <link>https://forem.com/htuohz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/htuohz"/>
    <language>en</language>
    <item>
      <title>When npm install Fails with SELF_SIGNED_CERT_IN_CHAIN in Corporate Networks (Zscaler + Node 22 Deep Dive)</title>
      <dc:creator>Tianhao Zhou</dc:creator>
      <pubDate>Sat, 14 Feb 2026 02:23:12 +0000</pubDate>
      <link>https://forem.com/htuohz/when-npm-install-fails-with-selfsignedcertinchain-in-corporate-networks-zscaler-node-22-deep-6f6</link>
      <guid>https://forem.com/htuohz/when-npm-install-fails-with-selfsignedcertinchain-in-corporate-networks-zscaler-node-22-deep-6f6</guid>
      <description>&lt;h2&gt;
  
  
  🚨 The Problem
&lt;/h2&gt;

&lt;p&gt;Suddenly, &lt;code&gt;npm install&lt;/code&gt; started failing across all projects with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SELF_SIGNED_CERT_IN_CHAIN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It wasn’t repo-specific.&lt;br&gt;
It wasn’t npm registry downtime.&lt;br&gt;
It wasn’t a corrupt cache.&lt;/p&gt;

&lt;p&gt;It happened everywhere.&lt;/p&gt;

&lt;p&gt;Environment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js v22.x&lt;/li&gt;
&lt;li&gt;npm v10.x&lt;/li&gt;
&lt;li&gt;Corporate network (Zscaler SSL Inspection)&lt;/li&gt;
&lt;li&gt;Windows + Git Bash&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  🔍 What Was Really Happening?
&lt;/h2&gt;

&lt;p&gt;Our company uses &lt;strong&gt;Zscaler SSL Inspection&lt;/strong&gt;, which performs TLS interception (MITM) for outbound HTTPS traffic.&lt;/p&gt;

&lt;p&gt;So instead of the normal TLS chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;registry.npmjs.org
    ↑
Public CA
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual chain becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;registry.npmjs.org (re-signed)
    ↑
Zscaler Intermediate
    ↑
Internal Enterprise CA
    ↑
Enterprise Root CA
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is normal in enterprise environments.&lt;/p&gt;




&lt;h2&gt;
  
  
  ❓ Why Did It Suddenly Break?
&lt;/h2&gt;

&lt;p&gt;The key trigger was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Node.js v22 tightened TLS validation behavior.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Older Node versions were more tolerant about incomplete chains.&lt;/p&gt;

&lt;p&gt;Node 22:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Uses OpenSSL 3.x&lt;/li&gt;
&lt;li&gt;Enforces stricter certificate validation&lt;/li&gt;
&lt;li&gt;Does not auto-reconstruct incomplete intermediate chains&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Providing only the enterprise root certificate is no longer sufficient.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔬 How I Diagnosed It
&lt;/h2&gt;

&lt;p&gt;First, I checked whether my local CA file contained a full chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"BEGIN CERTIFICATE"&lt;/span&gt; zscaler-root.cer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only one certificate.&lt;/p&gt;

&lt;p&gt;But when I inspected the live TLS connection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl s_client &lt;span class="nt"&gt;-showcerts&lt;/span&gt; &lt;span class="nt"&gt;-connect&lt;/span&gt; registry.npmjs.org:443 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-servername&lt;/span&gt; registry.npmjs.org &amp;lt; /dev/null &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/npm-chain.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"BEGIN CERTIFICATE"&lt;/span&gt; /tmp/npm-chain.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five certificates were involved in the trust chain.&lt;/p&gt;

&lt;p&gt;That was the smoking gun.&lt;/p&gt;




&lt;h2&gt;
  
  
  🛠 The Real Fix: Use the Full Certificate Chain
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1 – Split certificates
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'/BEGIN CERTIFICATE/{i++} {print &amp;gt; ("/c/certs/npm-chain-" i ".pem")}'&lt;/span&gt; /tmp/npm-chain.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2 – Combine into a full bundle
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /c/certs/npm-chain-1.pem &lt;span class="se"&gt;\&lt;/span&gt;
    /c/certs/npm-chain-2.pem &lt;span class="se"&gt;\&lt;/span&gt;
    /c/certs/npm-chain-3.pem &lt;span class="se"&gt;\&lt;/span&gt;
    /c/certs/npm-chain-4.pem &lt;span class="se"&gt;\&lt;/span&gt;
    /c/certs/npm-chain-5.pem &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /c/certs/npm-full-chain.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"BEGIN CERTIFICATE"&lt;/span&gt; /c/certs/npm-full-chain.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3 – Configure npm and Node
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm config &lt;span class="nb"&gt;set &lt;/span&gt;cafile &lt;span class="s2"&gt;"C:&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;certs&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;npm-full-chain.pem"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;NODE_EXTRA_CA_CERTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/c/certs/npm-full-chain.pem
npm cache clean &lt;span class="nt"&gt;--force&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Success.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 Why This Works
&lt;/h2&gt;

&lt;p&gt;Node validates certificate chains strictly.&lt;/p&gt;

&lt;p&gt;When behind corporate SSL inspection:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The leaf certificate is re-issued by Zscaler.&lt;/li&gt;
&lt;li&gt;Multiple internal CA layers are involved.&lt;/li&gt;
&lt;li&gt;Node must see the entire chain to validate trust.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any intermediate is missing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SELF_SIGNED_CERT_IN_CHAIN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Providing only the root CA is not enough in multi-level PKI environments.&lt;/p&gt;




&lt;h2&gt;
  
  
  💡 Why Browsers Worked
&lt;/h2&gt;

&lt;p&gt;Browsers use the Windows certificate store.&lt;/p&gt;

&lt;p&gt;Node does not (by default).&lt;/p&gt;

&lt;p&gt;So:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Windows trusts enterprise root CA&lt;/li&gt;
&lt;li&gt;Browser succeeds&lt;/li&gt;
&lt;li&gt;Node fails&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Different trust stores = different behavior.&lt;/p&gt;




&lt;h2&gt;
  
  
  🏢 Lessons for Enterprise Environments
&lt;/h2&gt;

&lt;p&gt;If you're behind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zscaler&lt;/li&gt;
&lt;li&gt;Blue Coat&lt;/li&gt;
&lt;li&gt;Palo Alto SSL Inspection&lt;/li&gt;
&lt;li&gt;Any corporate TLS interception&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node 20+&lt;/li&gt;
&lt;li&gt;Node 22+&lt;/li&gt;
&lt;li&gt;npm 10+&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You may need to provide a &lt;strong&gt;full certificate bundle&lt;/strong&gt;, not just a root certificate.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔥 Key Takeaway
&lt;/h2&gt;

&lt;p&gt;The error wasn't really about npm.&lt;/p&gt;

&lt;p&gt;It was about:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Enterprise TLS inspection + multi-level internal PKI + stricter Node.js TLS validation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once you understand the trust chain, the fix becomes straightforward.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 Final Thought
&lt;/h2&gt;

&lt;p&gt;If you see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SELF_SIGNED_CERT_IN_CHAIN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a corporate environment, don’t disable SSL verification.&lt;/p&gt;

&lt;p&gt;Instead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Inspect the live TLS chain&lt;/li&gt;
&lt;li&gt;Extract all certificates&lt;/li&gt;
&lt;li&gt;Provide a complete CA bundle to Node&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Security preserved. Problem solved.&lt;/p&gt;




&lt;p&gt;If you found this helpful, let me know.&lt;br&gt;
Enterprise TLS debugging is painful — but very educational.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>networking</category>
      <category>node</category>
      <category>security</category>
    </item>
    <item>
      <title>Revisiting My esbuild Configuration: Lessons Learned from Snowflake and Prisma</title>
      <dc:creator>Tianhao Zhou</dc:creator>
      <pubDate>Sun, 01 Feb 2026 10:22:56 +0000</pubDate>
      <link>https://forem.com/htuohz/revisiting-my-esbuild-configuration-lessons-learned-from-snowflake-and-prisma-40df</link>
      <guid>https://forem.com/htuohz/revisiting-my-esbuild-configuration-lessons-learned-from-snowflake-and-prisma-40df</guid>
      <description>&lt;p&gt;In my previous post about using esbuild with Serverless, I shared a configuration that worked well at the time. However, after introducing a new dependency (snowflake-sdk), I had to revisit that setup — and along the way, I discovered that some of my earlier assumptions were incomplete or even wrong.&lt;/p&gt;

&lt;p&gt;This post is a correction and clarification of that earlier article, based on what I learned during a much deeper debugging session.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Trigger: Introducing snowflake-sdk
&lt;/h3&gt;

&lt;p&gt;Recently, I needed to introduce a new dependency: snowflake-sdk.&lt;/p&gt;

&lt;p&gt;Because snowflake-sdk includes native .node binaries, I followed the common advice and added it to the external field in my esbuild configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;external:
  - snowflake-sdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The intention was simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Let esbuild skip bundling Snowflake, and load it at runtime instead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Unfortunately, this time the build failed.&lt;/p&gt;

&lt;p&gt;During compilation, esbuild encountered .node files and threw an error. This was my first signal that something fundamental had changed compared to my previous setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Temporary Fix That Created a Bigger Problem
&lt;/h3&gt;

&lt;p&gt;To unblock myself, I tried something that appeared to work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;I moved my esbuild configuration from custom.esbuild&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Back to top-level esbuild&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Surprisingly, the build passed.&lt;/p&gt;

&lt;p&gt;However, this introduced a much more serious issue.&lt;/p&gt;

&lt;p&gt;My previously working package.patterns configuration — which carefully excluded unnecessary Prisma files — stopped working entirely.&lt;/p&gt;

&lt;p&gt;As a result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A large number of unnecessary @prisma/client runtime files were bundled&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The final Lambda artifact grew far beyond AWS Lambda’s size limit&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I was stuck in a dilemma:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep Snowflake working but exceed Lambda limits&lt;/li&gt;
&lt;li&gt;Control bundle size but break Snowflake&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Clearly, I didn’t fully understand what was happening yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Key Question: Why Does esbuild Behave Differently at Top Level?
&lt;/h3&gt;

&lt;p&gt;At this point, the real question became:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Why does package.patterns work when esbuild is defined at the top level, but not when it lives under custom.esbuild?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After a lot of trial, error, and reading through build outputs, I realized the answer was not about package.patterns at all.&lt;/p&gt;

&lt;p&gt;The real issue was external.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Real Root Cause: external Changes the Packaging Model
&lt;/h3&gt;

&lt;p&gt;Here is the critical insight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;When a dependency (for example @prisma/client) is listed in external&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Serverless assumes it must be available at runtime&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;To guarantee this, it stops respecting package.patterns exclusions for that dependency&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Instead, it ensures the dependency (and its transitive files) are included in the final artifact&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;external implicitly overrides your intention to exclude files.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This explains everything.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why My Previous Setup “Worked by Accident”
&lt;/h3&gt;

&lt;p&gt;In my earlier build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;esbuild was configured at the top level&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;@prisma/client was listed in external&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I didn’t realize at the time was that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The top-level esbuild path caused Serverless to use a different internal build flow&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In that flow, external did not behave the same way&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;As a side effect, Prisma files were not force-included, and my bundle stayed small&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So my previous success was not due to a correct understanding — it was an accidental alignment of behaviors.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Correct Fix: Remove Prisma from external
&lt;/h3&gt;

&lt;p&gt;Once I understood this, the correct solution became clear.&lt;/p&gt;

&lt;p&gt;I tried one final experiment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep esbuild under custom.esbuild&lt;/li&gt;
&lt;li&gt;Remove @prisma/client from the external list&lt;/li&gt;
&lt;li&gt;Let esbuild bundle Prisma normally&lt;/li&gt;
&lt;li&gt;Keep package.patterns in place&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To my surprise — and relief — it worked.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The build succeeded&lt;/li&gt;
&lt;li&gt;Snowflake was handled separately&lt;/li&gt;
&lt;li&gt;Prisma files were properly tree-shaken&lt;/li&gt;
&lt;li&gt;The final bundle size stayed under 50 MB&lt;/li&gt;
&lt;li&gt;No Lambda size limits were violated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This time, it worked for the right reasons.&lt;/p&gt;

&lt;h3&gt;
  
  
  Final Takeaways
&lt;/h3&gt;

&lt;p&gt;Here are the key lessons I took away from this experience:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;external is not just a bundling hint&lt;br&gt;
It fundamentally changes how Serverless treats dependencies during packaging.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;package.patterns cannot override runtime guarantees&lt;br&gt;
If a dependency is marked as external, Serverless will ensure it exists — even if that means ignoring exclusions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Top-level esbuild vs custom.esbuild can change behavior&lt;br&gt;
Not because one is “better”, but because they trigger different internal build paths.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A working build does not always mean a correct build&lt;br&gt;
My earlier configuration worked by coincidence, not by design.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;This experience reminded me that build systems often fail silently — until they don’t. What looked like a simple Snowflake integration turned into a deep dive into how esbuild, Serverless, and packaging rules interact.&lt;/p&gt;

&lt;p&gt;Hopefully, this correction helps others avoid the same confusion — and encourages a bit of healthy skepticism when a configuration “just works” without a clear explanation.&lt;/p&gt;

&lt;p&gt;If you’re using esbuild with Serverless, especially with Prisma or native dependencies, understanding how external truly behaves is essential.&lt;/p&gt;

</description>
      <category>serverless</category>
      <category>snowflakesdk</category>
      <category>awslambda</category>
      <category>prisma</category>
    </item>
    <item>
      <title>What a Failed Concert Ticket Purchase Taught Me as a Full-Stack Developer</title>
      <dc:creator>Tianhao Zhou</dc:creator>
      <pubDate>Mon, 26 Jan 2026 09:07:38 +0000</pubDate>
      <link>https://forem.com/htuohz/what-a-failed-concert-ticket-purchase-taught-me-as-a-full-stack-developer-50g6</link>
      <guid>https://forem.com/htuohz/what-a-failed-concert-ticket-purchase-taught-me-as-a-full-stack-developer-50g6</guid>
      <description>&lt;p&gt;Last weekend, I tried to buy tickets for a highly anticipated concert.&lt;br&gt;
I didn’t get the ticket.&lt;/p&gt;

&lt;p&gt;But as a full-stack developer, I walked away with something far more valuable:&lt;br&gt;
a real-world lesson in how large-scale, high-concurrency systems actually fail.&lt;/p&gt;

&lt;p&gt;This wasn’t a simple “sold out in 30 seconds” scenario. The ticketing platform eventually paused sales entirely, citing backend overload and system instability. What I experienced in the browser—loading states, retries, timeouts, and silent failures—was a live demonstration of distributed systems under extreme pressure.&lt;/p&gt;

&lt;p&gt;Here’s what I learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frontend “Loading” States Are Really Backend State Machines
&lt;/h2&gt;

&lt;p&gt;From the user’s perspective, the page was just “loading”.&lt;/p&gt;

&lt;p&gt;From the Network tab, it was clear that the frontend was reflecting a backend state machine:&lt;/p&gt;

&lt;p&gt;verification requests&lt;/p&gt;

&lt;p&gt;re-verification phases&lt;/p&gt;

&lt;p&gt;long-polling&lt;/p&gt;

&lt;p&gt;silent timeouts&lt;/p&gt;

&lt;p&gt;eventual gateway failures&lt;/p&gt;

&lt;p&gt;What looked like a spinner was actually the UI’s only way to represent:&lt;/p&gt;

&lt;p&gt;“Your session may or may not still be eligible.”&lt;/p&gt;

&lt;h3&gt;
  
  
  Takeaway:
&lt;/h3&gt;

&lt;p&gt;As frontend or full-stack developers, we’re not building buttons—we’re visualizing backend state transitions. If the state model is unclear, the UX will be confusing no matter how pretty the UI is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automatic Polling Is Normal at Scale (Even If It Feels Broken)
&lt;/h2&gt;

&lt;p&gt;The page didn’t reload, but requests kept happening in the background.&lt;/p&gt;

&lt;p&gt;This is typical for:&lt;/p&gt;

&lt;p&gt;queue systems&lt;/p&gt;

&lt;p&gt;long-polling&lt;/p&gt;

&lt;p&gt;heartbeat-based eligibility checks&lt;/p&gt;

&lt;p&gt;When systems are under extreme load, pushing state changes to clients is expensive, so the burden shifts to the client to keep asking.&lt;/p&gt;

&lt;h3&gt;
  
  
  Takeaway:
&lt;/h3&gt;

&lt;p&gt;“Nothing is happening” often means “the system is busy deciding.”&lt;br&gt;
Not all progress is visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  A CORS Error Is Sometimes a Business Decision, Not a Config Bug
&lt;/h2&gt;

&lt;p&gt;At one point, a critical verification request started returning a CORS error.&lt;/p&gt;

&lt;p&gt;At first glance, this looks like a misconfiguration.&lt;br&gt;
In reality, it often means:&lt;/p&gt;

&lt;p&gt;the upstream service timed out or dropped the request&lt;/p&gt;

&lt;p&gt;the edge layer returned a response without CORS headers&lt;/p&gt;

&lt;p&gt;the browser blocked access to the response&lt;/p&gt;

&lt;p&gt;In other words:&lt;/p&gt;

&lt;p&gt;the system no longer considers your session worth responding to.&lt;/p&gt;

&lt;h3&gt;
  
  
  Takeaway:
&lt;/h3&gt;

&lt;p&gt;Not every CORS error is a frontend mistake. In distributed systems, it can be the visible symptom of a backend refusal.&lt;/p&gt;

&lt;h2&gt;
  
  
  504 Gateway Timeout Is Sometimes a Polite “No”
&lt;/h2&gt;

&lt;p&gt;A 504 error doesn’t always mean the server is slow.&lt;/p&gt;

&lt;p&gt;In queue-based, fairness-critical systems, it can mean:&lt;/p&gt;

&lt;p&gt;the system re-evaluated active sessions&lt;/p&gt;

&lt;p&gt;your session didn’t make the cut&lt;/p&gt;

&lt;p&gt;the backend stopped responding intentionally&lt;/p&gt;

&lt;p&gt;the gateway timed out waiting&lt;/p&gt;

&lt;p&gt;This is a soft failure, not a crash.&lt;/p&gt;

&lt;h3&gt;
  
  
  Takeaway:
&lt;/h3&gt;

&lt;p&gt;Some HTTP errors are business outcomes disguised as infrastructure failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Queues Are Rarely FIFO in the Real World
&lt;/h2&gt;

&lt;p&gt;We like to think queues are first-come, first-served.&lt;/p&gt;

&lt;p&gt;In practice, eligibility is constantly re-evaluated based on:&lt;/p&gt;

&lt;p&gt;session stability&lt;/p&gt;

&lt;p&gt;retry behavior&lt;/p&gt;

&lt;p&gt;network latency&lt;/p&gt;

&lt;p&gt;concurrency from the same account or IP&lt;/p&gt;

&lt;p&gt;risk or fairness heuristics&lt;/p&gt;

&lt;p&gt;The queue is not a line—it’s a dynamic eligibility pool.&lt;/p&gt;

&lt;h3&gt;
  
  
  Takeaway:
&lt;/h3&gt;

&lt;p&gt;If fairness matters, strict FIFO often doesn’t scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Systems Sometimes Prefer Downtime Over Unfair Success
&lt;/h2&gt;

&lt;p&gt;Eventually, the ticketing platform halted sales completely.&lt;/p&gt;

&lt;p&gt;This decision said a lot:&lt;/p&gt;

&lt;p&gt;partial success was happening&lt;/p&gt;

&lt;p&gt;many users were stuck mid-transaction&lt;/p&gt;

&lt;p&gt;continuing would create unfair outcomes&lt;/p&gt;

&lt;p&gt;trust would be damaged&lt;/p&gt;

&lt;p&gt;So they chose consistency and integrity over availability.&lt;/p&gt;

&lt;h3&gt;
  
  
  Takeaway:
&lt;/h3&gt;

&lt;p&gt;In high-stakes systems, fairness can be more important than uptime.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Worst Failures Are Silent Ones
&lt;/h2&gt;

&lt;p&gt;What made the experience frustrating wasn’t the failure—it was the ambiguity.&lt;/p&gt;

&lt;p&gt;No clear “you’re out” message&lt;/p&gt;

&lt;p&gt;No explicit retry guidance&lt;/p&gt;

&lt;p&gt;Just endless waiting or vague errors&lt;/p&gt;

&lt;p&gt;From a UX perspective, this is painful.&lt;/p&gt;

&lt;h3&gt;
  
  
  Takeaway:
&lt;/h3&gt;

&lt;p&gt;Silent failures erode trust more than explicit errors.&lt;br&gt;
Clear state communication is part of system reliability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Users Will Do More Than You Expect
&lt;/h2&gt;

&lt;p&gt;People don’t just click buttons:&lt;/p&gt;

&lt;p&gt;they open multiple tabs&lt;/p&gt;

&lt;p&gt;switch networks&lt;/p&gt;

&lt;p&gt;inspect requests&lt;/p&gt;

&lt;p&gt;wait strategically&lt;/p&gt;

&lt;p&gt;retry at specific moments&lt;/p&gt;

&lt;p&gt;Your system isn’t just used—it’s interpreted.&lt;/p&gt;

&lt;p&gt;Takeaway:&lt;br&gt;
Design systems assuming users are curious, persistent, and adaptive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Incident Communication Is Part of the System
&lt;/h2&gt;

&lt;p&gt;After the failure, the company released a public statement explaining:&lt;/p&gt;

&lt;p&gt;what happened&lt;/p&gt;

&lt;p&gt;why sales were paused&lt;/p&gt;

&lt;p&gt;that integrity mattered&lt;/p&gt;

&lt;p&gt;that the issue would be resolved&lt;/p&gt;

&lt;p&gt;This wasn’t just PR.&lt;br&gt;
It was incident response and trust repair.&lt;/p&gt;

&lt;h3&gt;
  
  
  Takeaway:
&lt;/h3&gt;

&lt;p&gt;A system doesn’t end at the API boundary. Communication is part of reliability.&lt;/p&gt;

&lt;h2&gt;
  
  
  This Was a Real Production Incident, Not a Thought Experiment
&lt;/h2&gt;

&lt;p&gt;Many developers never experience a true traffic surge incident firsthand.&lt;/p&gt;

&lt;p&gt;This one had:&lt;/p&gt;

&lt;p&gt;money&lt;/p&gt;

&lt;p&gt;fairness constraints&lt;/p&gt;

&lt;p&gt;global traffic&lt;/p&gt;

&lt;p&gt;human emotion&lt;/p&gt;

&lt;p&gt;executive intervention&lt;/p&gt;

&lt;p&gt;Watching a system bend—and break—under real pressure is an education you can’t get from tutorials.&lt;/p&gt;

&lt;p&gt;Final takeaway:&lt;br&gt;
I didn’t get a concert ticket.&lt;br&gt;
But I gained a deeper understanding of distributed systems, failure modes, and user trust.&lt;/p&gt;

&lt;p&gt;That’s a trade I’ll take.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>performance</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>How I Finally Fixed My Serverless + esbuild + Prisma Packaging Nightmare</title>
      <dc:creator>Tianhao Zhou</dc:creator>
      <pubDate>Thu, 15 Jan 2026 10:23:38 +0000</pubDate>
      <link>https://forem.com/htuohz/how-i-finally-fixed-my-serverless-esbuild-prisma-packaging-nightmare-ah0</link>
      <guid>https://forem.com/htuohz/how-i-finally-fixed-my-serverless-esbuild-prisma-packaging-nightmare-ah0</guid>
      <description>&lt;p&gt;Here is a &lt;strong&gt;short, clean, blog‑friendly version&lt;/strong&gt; that reflects your actual discovery:&lt;/p&gt;




&lt;h1&gt;
  
  
  &lt;strong&gt;How I Shrunk My Serverless Bundle by Discovering One Unexpected esbuild Issue&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;Today I finally solved a packaging problem that had bothered me for weeks:&lt;br&gt;&lt;br&gt;
no matter how many &lt;code&gt;package.patterns&lt;/code&gt; I added — excluding Prisma engines, WASM files, TypeScript runtime, sourcemaps, caches — &lt;strong&gt;my AWS Lambda bundle never changed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It didn’t matter if I excluded a few files or &lt;strong&gt;excluded everything&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
The ZIP size stayed exactly the same.&lt;/p&gt;

&lt;p&gt;Surprisingly, the fix had nothing to do with tree‑shaking, Prisma engine types, or exclude patterns.&lt;br&gt;&lt;br&gt;
The real culprit was something much simpler.&lt;/p&gt;


&lt;h2&gt;
  
  
  &lt;strong&gt;Problem: &lt;code&gt;package.patterns&lt;/code&gt; didn’t work at all&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;I kept seeing massive files in the final ZIP:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;@prisma/engines/**&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;query_engine_bg.*.wasm-base64.js&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;*.wasm&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;typescript/**&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;prisma/**&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even if I removed &lt;em&gt;all&lt;/em&gt; patterns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;package&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;patterns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ZIP was still identical.&lt;/p&gt;

&lt;p&gt;That meant Serverless was completely ignoring my configuration.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Root Cause: esbuild never ran&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;After turning on verbose mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx serverless package &lt;span class="nt"&gt;--verbose&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I saw &lt;strong&gt;no esbuild activity&lt;/strong&gt; in the logs.&lt;/p&gt;

&lt;p&gt;That’s when I realized:&lt;/p&gt;

&lt;p&gt;My esbuild configuration was written under the wrong key.&lt;/p&gt;

&lt;p&gt;I had something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;custom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;something&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;esbuild&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;bundle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the &lt;code&gt;serverless-esbuild&lt;/code&gt; plugin &lt;strong&gt;only reads config in one of two places&lt;/strong&gt;:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Correct option A&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;custom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;esbuild&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;bundle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Correct option B (newer style)&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;esbuild&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;bundle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once I moved the config to the correct top‑level (sibling of &lt;code&gt;custom&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;esbuild&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;bundle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;minify&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;— &lt;strong&gt;everything started working immediately&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Serverless finally invoked esbuild, generated &lt;code&gt;.cjs&lt;/code&gt; bundles, and only then did &lt;code&gt;package.patterns&lt;/code&gt; begin affecting the ZIP.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Important: This had nothing to do with Prisma engine type&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;I originally thought the fix required switching Prisma to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;engineType = "client"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But that wasn’t necessary at all.&lt;/p&gt;

&lt;p&gt;The only real fix was:&lt;/p&gt;

&lt;h3&gt;
  
  
  ✔️ Move &lt;code&gt;esbuild:&lt;/code&gt; to the correct level in &lt;code&gt;serverless.yml&lt;/code&gt;.
&lt;/h3&gt;

&lt;p&gt;Once esbuild finally ran:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  The bundle size changed&lt;/li&gt;
&lt;li&gt;  Patterns were applied correctly&lt;/li&gt;
&lt;li&gt;  Unwanted files (WASM, engines, TypeScript) were finally excluded&lt;/li&gt;
&lt;li&gt;  The ZIP output became predictable and small&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;&lt;code&gt;serverless-esbuild&lt;/code&gt; only executes when it finds config at the correct path.&lt;br&gt;&lt;br&gt;
If the plugin can’t see your config:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; esbuild never runs&lt;/li&gt;
&lt;li&gt; Serverless zips raw source + raw node_modules&lt;/li&gt;
&lt;li&gt; All &lt;code&gt;package.patterns&lt;/code&gt; appear “broken” because they’re being applied to the wrong file structure&lt;/li&gt;
&lt;li&gt; The ZIP never changes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Fixing the config path fixes &lt;em&gt;everything&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Takeaway&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;If your Serverless bundle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  is too big&lt;/li&gt;
&lt;li&gt;  ignores your exclude rules&lt;/li&gt;
&lt;li&gt;  keeps including Prisma engines, WASM, or TypeScript&lt;/li&gt;
&lt;li&gt;  or looks unchanged no matter what you do&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check this first:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Is your &lt;code&gt;esbuild:&lt;/code&gt; block in the correct location?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;If not, nothing else will behave as expected.&lt;/p&gt;

&lt;p&gt;This one small change made the entire packaging pipeline work properly again.&lt;/p&gt;




</description>
      <category>aws</category>
      <category>devops</category>
      <category>performance</category>
      <category>serverless</category>
    </item>
    <item>
      <title>How I finally understood when I can use useRef()</title>
      <dc:creator>Tianhao Zhou</dc:creator>
      <pubDate>Sun, 05 Jun 2022 01:39:54 +0000</pubDate>
      <link>https://forem.com/htuohz/how-i-finally-understood-when-i-can-use-useref-5360</link>
      <guid>https://forem.com/htuohz/how-i-finally-understood-when-i-can-use-useref-5360</guid>
      <description>&lt;p&gt;Currently I've got a ticket to build a react page with a map view.&lt;/p&gt;

&lt;p&gt;Since it's a react app, I chose to use the Mapbox GL JS package. It allows you to load the map view as well as add custom markers.&lt;/p&gt;

&lt;p&gt;In my case I have a list of companies to show in this app, and I also need to render the markers for each of the companies. At the same time I need to have filters on the result of companies. So the markers need to be re-rendered every time the results get filtered. &lt;/p&gt;

&lt;p&gt;That requires me to remove the markers when I need to render new markers. So I will need an array to store the markers. I tried to declare a global variable first, but it wouldn't work, I guess it had something to do with the closure issue. Then I did a search using the key words such as 'how can I have properties in a function component'. Then I got the answer that I should can declare an array like:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;const markers = useRef([])&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;And when I need to access it I will have to use&lt;/p&gt;

&lt;p&gt;&lt;code&gt;markers.current&lt;/code&gt; instead of &lt;code&gt;markers&lt;/code&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>beginners</category>
      <category>react</category>
    </item>
    <item>
      <title>Replaced curl-request with http module.</title>
      <dc:creator>Tianhao Zhou</dc:creator>
      <pubDate>Sat, 04 Dec 2021 01:15:20 +0000</pubDate>
      <link>https://forem.com/htuohz/replaced-curl-request-with-http-module-411d</link>
      <guid>https://forem.com/htuohz/replaced-curl-request-with-http-module-411d</guid>
      <description>&lt;p&gt;I'm currently working on a celebrity face recognition app, leveraging the api provided by Clarifai team. &lt;/p&gt;

&lt;p&gt;This api returns a list of candidates' name when you send a image url with celebrities face(s) to it. &lt;/p&gt;

&lt;p&gt;To increase the performance, I decided to not only show the names of the candidates, but also attach a thumbnail of the celebrity, like this:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbvyx9aes74f5cv2wu6s2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbvyx9aes74f5cv2wu6s2.png" alt=" " width="680" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The solution came up to my mind is using wikipedia to get the profile page of that celebrity, which normally contains an image of the celebrity as well.&lt;/p&gt;

&lt;p&gt;But how should I parse the page and retrieve the thumbnail url from it? &lt;/p&gt;

&lt;p&gt;I tried to use the curl-request package first, which worked well locally. But when I tried to deploy it on Heroku, I got a whole bunch of errors. It turned out that one of the dependancy package of curl-request - node-gyp is not supported by node higher than 10.x, which is sebsequently not supported by npm higher than v6. When I use npm 6.x to build the project, I also get error from heroku.&lt;/p&gt;

&lt;p&gt;To solve this conflict, I started to think about whether I really need to use curl-request. And the answer is No. &lt;/p&gt;

&lt;p&gt;After googling, I found this solution - using http module to get the html code then parse it. &lt;/p&gt;


&lt;div class="ltag__stackexchange--container"&gt;
  &lt;div class="ltag__stackexchange--title-container"&gt;
    
      &lt;div class="ltag__stackexchange--title"&gt;
        &lt;div class="ltag__stackexchange--header"&gt;
          &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fstackoverflow-logo-b42691ae545e4810b105ee957979a853a696085e67e43ee14c5699cf3e890fb4.svg" alt=""&gt;
          &lt;a href="https://stackoverflow.com/questions/6287297/reading-content-from-url-with-node-js/64038999#64038999" rel="noopener noreferrer"&gt;
            &lt;span class="title-flare"&gt;answer&lt;/span&gt; re: Reading content from URL with Node.js
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="ltag__stackexchange--post-metadata"&gt;
          &lt;span&gt;Sep 24 '20&lt;/span&gt;
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;a class="ltag__stackexchange--score-container" href="https://stackoverflow.com/questions/6287297/reading-content-from-url-with-node-js/64038999#64038999" rel="noopener noreferrer"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fstackexchange-arrow-up-eff2e2849e67d156181d258e38802c0b57fa011f74164a7f97675ca3b6ab756b.svg" alt=""&gt;
        &lt;div class="ltag__stackexchange--score-number"&gt;
          9
        &lt;/div&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fstackexchange-arrow-down-4349fac0dd932d284fab7e4dd9846f19a3710558efde0d2dfd05897f3eeb9aba.svg" alt=""&gt;
      &lt;/a&gt;
    
  &lt;/div&gt;
  &lt;div class="ltag__stackexchange--body"&gt;
    
&lt;p&gt;A slightly modified version of @sidanmor 's code. The main point is, not every webpage is purely ASCII, user should be able to handle the decoding manually (even encode into base64)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function httpGet(url) {
  return new Promise((resolve, reject) =&amp;gt; {
    const http = require('http')
      https = require('https');

    let client =&lt;/code&gt;&lt;/pre&gt;…
    
  &lt;/div&gt;
  &lt;div class="ltag__stackexchange--btn--container"&gt;
    &lt;a href="https://stackoverflow.com/questions/6287297/reading-content-from-url-with-node-js/64038999#64038999" class="ltag__stackexchange--btn" rel="noopener noreferrer"&gt;Open Full Answer&lt;/a&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Up until now I've understood why we have to think carefully when we are adding a new package. Because when you add a new package, it means you are being dependant on it. Once it's deprecated, you might get trouble. &lt;/p&gt;

</description>
      <category>node</category>
      <category>express</category>
    </item>
  </channel>
</rss>
