<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[SiloSave Engineering]]></title><description><![CDATA[SiloSave Engineering]]></description><link>https://silosave.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Fri, 30 Oct 2026 12:20:56 GMT</lastBuildDate><atom:link href="https://silosave.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Why a CDN image opens in a tab but fails in a browser ZIP]]></title><description><![CDATA[Learn why CDN links can open while ZIP fetches fail, and how to handle CORS, opaque responses, size checks, and useful fallbacks.]]></description><link>https://silosave.hashnode.dev/why-a-cdn-image-opens-in-a-tab-but-fails-in-a-browser-zip</link><guid isPermaLink="true">https://silosave.hashnode.dev/why-a-cdn-image-opens-in-a-tab-but-fails-in-a-browser-zip</guid><category><![CDATA[JavaScript]]></category><category><![CDATA[CORS]]></category><dc:creator><![CDATA[SiloSave Team]]></dc:creator><pubDate>Tue, 27 Oct 2026 13:54:04 GMT</pubDate><content:encoded><![CDATA[<p>A working image link does not prove that your JavaScript can read the image bytes.</p>
<p>That distinction matters when a web app offers both “open original” and “download ZIP.” The first action can navigate to a resource. The second needs to fetch its contents, collect the bytes, and build an archive. A successful navigation does not establish permission for that cross-origin script access.</p>
<p>Disclosure: I work on SiloSave. This engineering note was prepared with AI assistance and checked against our browser ZIP implementation and the documentation linked below.</p>
<h2>Separate the two paths</h2>
<p>Think of the interface as two independent operations:</p>
<ol>
<li><strong>Open an original file:</strong> let the browser navigate to the source URL.</li>
<li><strong>Build a ZIP:</strong> use JavaScript to read every selected response body, then pass the bytes to an archive writer.</li>
</ol>
<p>The second path depends on the remote server's cross-origin response policy. An image displaying successfully in a tab is therefore an incomplete test of a ZIP feature. MDN's <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS">CORS guide</a> explains how servers grant cross-origin script access through response headers.</p>
<h2>Do not replace a failed CORS request with no-cors</h2>
<p>Changing a request to <code>mode: "no-cors"</code> can look tempting when a cross-origin fetch fails. It does not give an archive writer readable bytes. The resulting opaque response hides the body and headers from JavaScript. See MDN's <a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#checking_the_response_type">Fetch response types</a>.</p>
<p>A small, illustrative fetch boundary can look like this:</p>
<pre><code class="language-js">async function readPublicFile(url) {
  const response = await fetch(url, {
    mode: "cors",
    credentials: "omit",
    referrerPolicy: "no-referrer",
  });
  if (!response.ok || response.type === "opaque") {
    throw new Error("File response is not readable");
  }
  return new Uint8Array(await response.arrayBuffer());
}
</code></pre>
<p>This is only the read boundary. It is not a complete downloader: it does not include a URL policy, cancellation, size limits, retries, or ZIP generation. It assumes a public resource that does not require credentials; omitting credentials will not make authenticated files available.</p>
<h2>Treat size metadata as a hint</h2>
<p>Our ZIP builder can request declared sizes before downloading the bodies. That can reject an obviously oversized selection early. But a failed HEAD request, a missing length, or a different GET response still has to be handled.</p>
<p>Check the bytes actually read as well. A missing size is unknown, not evidence that a file is empty.</p>
<p>There is another limit to keep visible: checking <code>arrayBuffer().byteLength</code> happens after that body has been buffered. It can enforce an acceptance rule, but it is not a hard limit on peak browser memory. The source arrays and the generated archive can also coexist. A streaming design needs its own cancellation and memory tests.</p>
<h2>Keep filenames and progress honest</h2>
<p>Two files with the same proposed name can overwrite each other in a filename-keyed map before the ZIP is generated. Allocate a unique archive name for each file, such as <code>photo.jpg</code> and <code>photo-2.jpg</code>.</p>
<p>Separate the “reading files” and “building archive” phases in the progress display. Reaching the last network response is not the same as having a downloadable archive ready.</p>
<h2>Offer a useful failure path</h2>
<p>If a batch cannot be read, preserve the source information and the individual original links when those are available. Explain that the browser could not build this ZIP. Do not automatically label the original post as deleted or private.</p>
<p>A rejected fetch alone also does not prove CORS was the cause. Network failures and browser restrictions can produce similar symptoms; inspect the console and network details when diagnosing the specific failure.</p>
<p>For our product's accepted URLs and browser export limits, we maintain a <a href="https://silosave.com/threads-downloader-compatibility?utm_source=hashnode&amp;utm_medium=referral&amp;utm_campaign=browser_zip_cors_note">Threads compatibility reference</a>. The general design lesson is to represent direct navigation and script-readable downloads as separate capabilities.</p>
<p>Before shipping, test a readable response, a rejected fetch, an oversized declared response, duplicate names, and progress transitions. Mocked unit tests can check your handling of these cases. A real browser test against two origins you control is still needed to verify actual CORS enforcement.
 </p>
]]></content:encoded></item></channel></rss>