<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Foretaste</title>
    <description>The latest articles on DEV Community by Foretaste (@foretaste).</description>
    <link>https://dev.to/foretaste</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4168809%2Fc1a6e374-1424-4984-8141-214523c20d36.png</url>
      <title>DEV Community: Foretaste</title>
      <link>https://dev.to/foretaste</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/foretaste"/>
    <language>en</language>
    <item>
      <title>Why Browsers Force File Downloads and How to Preview Binary Data Client-Side</title>
      <dc:creator>Foretaste</dc:creator>
      <pubDate>Tue, 13 Oct 2026 04:26:27 +0000</pubDate>
      <link>https://dev.to/foretaste/why-browsers-force-file-downloads-and-how-to-preview-binary-data-client-side-5804</link>
      <guid>https://dev.to/foretaste/why-browsers-force-file-downloads-and-how-to-preview-binary-data-client-side-5804</guid>
      <description>&lt;p&gt;Every developer knows this scenario. You are browsing GitHub, an internal documentation portal, or an S3 bucket console. You see a link to a document, a dataset, or a config file. You only want to inspect it for five seconds to check a column name, read a changelog, or verify a number.&lt;/p&gt;

&lt;p&gt;You click the link. Instead of opening cleanly in a new browser tab, the browser triggers an immediate download to your hard drive.&lt;/p&gt;

&lt;p&gt;By Friday afternoon, your Downloads folder contains twenty files you never intended to keep: three versions of a customer export, five PDFs, and an assortment of &lt;code&gt;.sqlite&lt;/code&gt;, &lt;code&gt;.parquet&lt;/code&gt;, and &lt;code&gt;.docx&lt;/code&gt; files.&lt;/p&gt;

&lt;p&gt;Here is why browsers behave this way, why blindly downloading files is a real security concern, and how you can parse and preview complex binary formats completely in-browser without sending a single byte to an external server.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Browsers Force Downloads Instead of Showing Files
&lt;/h2&gt;

&lt;p&gt;Browsers do not download files by accident. They download them when one of three web standards tells them to:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Content-Disposition Header
&lt;/h3&gt;

&lt;p&gt;Web servers often send this header in their HTTP responses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Content-Disposition: attachment; filename="report-q3.xlsx"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The keyword &lt;code&gt;attachment&lt;/code&gt; instructs the browser to treat the incoming payload as a file download rather than rendering it in the viewport, regardless of whether the browser natively knows how to display the format.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The HTML5 Download Attribute
&lt;/h3&gt;

&lt;p&gt;If an HTML link has the &lt;code&gt;download&lt;/code&gt; attribute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/data/analytics.parquet"&lt;/span&gt; &lt;span class="na"&gt;download&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Download Dataset&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser obeys the author's directive and routes the incoming stream directly to the local disk.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Generic or Missing MIME Types
&lt;/h3&gt;

&lt;p&gt;If an S3 bucket or API endpoint sends a file with &lt;code&gt;Content-Type: application/octet-stream&lt;/code&gt; or no MIME type at all, the browser treats it as unknown raw binary data. Because it does not know what renderer to invoke, it dumps the stream into your Downloads directory.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Hidden Security and Privacy Risks
&lt;/h2&gt;

&lt;p&gt;Forced downloads are not just an annoyance. They create real security and privacy risks.&lt;/p&gt;

&lt;h3&gt;
  
  
  The SVG Script Execution Trap
&lt;/h3&gt;

&lt;p&gt;Most people treat SVG files like ordinary images such as PNGs or JPEGs. But SVGs are XML documents, and XML allows embedded &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags, inline event handlers like &lt;code&gt;&amp;lt;rect onload="..."&amp;gt;&lt;/code&gt;, and external resource fetches.&lt;/p&gt;

&lt;p&gt;If you open an untrusted SVG directly in a browser tab or download it to your local environment, any JavaScript embedded inside that SVG executes within the origin context. This can expose session cookies or trigger cross-site scripting (XSS).&lt;/p&gt;

&lt;p&gt;To render an SVG safely, it must be isolated inside a sandboxed iframe with scripts explicitly disabled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;iframe&lt;/span&gt; &lt;span class="na"&gt;sandbox=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="na"&gt;srcdoc=&lt;/span&gt;&lt;span class="s"&gt;"...sanitized SVG markup..."&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Magic Bytes vs. File Extension Spoofing
&lt;/h3&gt;

&lt;p&gt;Relying on a file extension in a URL is unsafe. An attacker can name a link &lt;code&gt;financial_report.pdf&lt;/code&gt; while serving a Windows binary or a malicious script.&lt;/p&gt;

&lt;p&gt;A secure preview engine does not trust URL endings. Instead, it inspects the first few magic bytes using a 512-byte HTTP Range request before deciding what to do with the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;detectRealMimeType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;headBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchByteRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Check PDF signature: %PDF- (0x25 0x50 0x44 0x46 0x2D)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mh"&gt;0x25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x46&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x2d&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Check SQLite format 3 signature: 'SQLite format 3\0'&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mh"&gt;0x53&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x51&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x4c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x69&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x74&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x65&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x66&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x6f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x72&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x6d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x61&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x74&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x33&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x00&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/x-sqlite3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Check Parquet magic bytes: 'PAR1' (0x50 0x41 0x52 0x31)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mh"&gt;0x50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x41&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x52&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x31&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/vnd.apache.parquet&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Check ZIP / Office container: 'PK\x03\x04'&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mh"&gt;0x50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x4b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x03&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x04&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/zip&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/octet-stream&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Third-Party Cloud Viewer Danger
&lt;/h3&gt;

&lt;p&gt;When faced with files they cannot open natively, many developers and office workers turn to free online viewers like "Free Online SQLite Viewer" or "Docx to HTML Online".&lt;/p&gt;

&lt;p&gt;This is a massive privacy risk. Uploading internal company database exports, financial sheets, or customer lists to random third-party cloud servers breaks confidentiality agreements and leaks private data.&lt;/p&gt;

&lt;p&gt;The only safe way to preview remote files without downloading them is to do all the parsing client-side, inside your own browser tab.&lt;/p&gt;




&lt;h2&gt;
  
  
  Peeking Into Complex Formats Locally
&lt;/h2&gt;

&lt;p&gt;Modern browsers are capable of running complex binary parsers and WebAssembly runtimes entirely in memory. Here is how three major formats can be previewed without downloading full files.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Parquet Datasets: Reading the Footer
&lt;/h3&gt;

&lt;p&gt;Apache Parquet is the standard format for big data analytics. A single Parquet file can easily be 500 MB or 2 GB. &lt;/p&gt;

&lt;p&gt;Similar to ZIP files, Parquet stores its file metadata in a footer at the very end of the file. The last 4 bytes of any valid Parquet file are the magic characters &lt;code&gt;PAR1&lt;/code&gt;. Immediately preceding those 4 bytes is a 4-byte little-endian integer indicating the byte length of the metadata footer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ Data Page 1 ]
[ Data Page 2 ]
...
[ FileMetaData (Thrift encoded) ]
[ 4-byte Footer Length ]
[ 4-byte Magic 'PAR1' ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To inspect schema, column types, row counts, and compression codecs for a 2 GB Parquet file, you only need to fetch the last few kilobytes over an HTTP Range request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;readParquetFooter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fileSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. Fetch the last 8 bytes to get the footer length&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchByteRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fileSize&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fileSize&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DataView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;byteOffset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;footerLen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Fetch only the metadata footer bytes&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;footerBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchByteRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fileSize&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;footerLen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fileSize&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. Decode Thrift metadata in browser memory&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;decodeParquetMetadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;footerBytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can view column names, data types, and row group counts instantly, without transferring the underlying gigabytes of column data.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. SQLite Databases: Paging Tables Without Full Downloads
&lt;/h3&gt;

&lt;p&gt;If you encounter a remote SQLite database on an S3 bucket or a staging server, you do not need to download the whole database file to run &lt;code&gt;SELECT&lt;/code&gt; queries or inspect the schema.&lt;/p&gt;

&lt;p&gt;The SQLite binary header is exactly 100 bytes at offset 0. It tells you the database page size (typically 4,096 bytes). Page 1 always contains the master &lt;code&gt;sqlite_schema&lt;/code&gt; table.&lt;/p&gt;

&lt;p&gt;By making targeted HTTP Range requests for individual 4 KB pages, you can parse B-tree nodes in browser memory. When a user requests table names, you read Page 1. When they page through the first 50 rows of a specific table, you fetch only the specific leaf page corresponding to those rows.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Paging Giant CSV Files
&lt;/h3&gt;

&lt;p&gt;CSV files do not have indexes at the tail. If someone gives you a 150 MB CSV file with 500,000 rows, rendering 500,000 DOM elements will instantly crash your browser tab.&lt;/p&gt;

&lt;p&gt;The correct approach is streaming with offset indexing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Stream the CSV bytes once to record the byte offsets of every 200th line.&lt;/li&gt;
&lt;li&gt;Store only those numeric offsets in memory (a few kilobytes of array data).&lt;/li&gt;
&lt;li&gt;Discard the raw text payload.&lt;/li&gt;
&lt;li&gt;When the user navigates to page 45, request only the byte range between offset &lt;code&gt;N&lt;/code&gt; and offset &lt;code&gt;N+1&lt;/code&gt; using an HTTP Range request.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Row 500,000 costs the exact same memory and rendering time as row 1.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to Handle This in Real Workflows
&lt;/h2&gt;

&lt;p&gt;To make this seamless in everyday development, you need an extension that intercepts link interactions before the browser's download manager takes over.&lt;/p&gt;

&lt;p&gt;I built this into &lt;a href="https://foretaste.page" rel="noopener noreferrer"&gt;Foretaste&lt;/a&gt;, a browser extension available for Chrome, Edge, and Firefox.&lt;/p&gt;

&lt;p&gt;Here is how it works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You right-click any link pointing to a file on the web.&lt;/li&gt;
&lt;li&gt;Foretaste sends an initial HTTP Range request to inspect magic bytes and headers.&lt;/li&gt;
&lt;li&gt;It opens an isolated preview tab inside your browser and renders the file using local parsers (Preact, WebAssembly, and native Streams).&lt;/li&gt;
&lt;li&gt;For ZIP archives, it reads the Central Directory and lets you browse or extract single files.&lt;/li&gt;
&lt;li&gt;For PDFs, Word documents, Excel workbooks, Parquet files, and SQLite databases, it renders the contents on the fly.&lt;/li&gt;
&lt;li&gt;Crucially, &lt;strong&gt;zero file bytes are uploaded to any server&lt;/strong&gt;. There is no backend processing service. Everything stays in your browser memory.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The web was originally built to let us read and browse documents, not force us into managing local filesystem clutter. &lt;/p&gt;

&lt;p&gt;By taking advantage of modern HTTP Range requests, browser Streams, and client-side binary parsers, we can preview remote files instantly and safely, without wasting bandwidth or compromising our privacy.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>security</category>
      <category>browserextension</category>
    </item>
    <item>
      <title>Listing a 40 MB ZIP Over HTTP by Reading Only 67 KB of It</title>
      <dc:creator>Foretaste</dc:creator>
      <pubDate>Wed, 07 Oct 2026 12:14:05 +0000</pubDate>
      <link>https://dev.to/foretaste/listing-a-40-mb-zip-over-http-by-reading-only-67-kb-of-it-ed1</link>
      <guid>https://dev.to/foretaste/listing-a-40-mb-zip-over-http-by-reading-only-67-kb-of-it-ed1</guid>
      <description>&lt;p&gt;Normally, if you want to see what is inside a remote &lt;code&gt;.zip&lt;/code&gt; file on the web, you have to download the whole file to your disk or upload it to a cloud tool. If the archive is 40 MB or 1 GB and you only need to check one file name or grab a single text file, downloading the whole thing is slow and wastes bandwidth.&lt;/p&gt;

&lt;p&gt;Here is how you can read the entire directory listing of a remote ZIP by fetching only ~67 KB over HTTP using byte range requests, without downloading the full archive.&lt;/p&gt;




&lt;h3&gt;
  
  
  Key Metric / Real Measurement
&lt;/h3&gt;

&lt;p&gt;Tested against a remote server logging all request payloads:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Archive total size:&lt;/strong&gt; 40,013,838 bytes (~40 MB) with 10 files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bytes fetched to list all files:&lt;/strong&gt; &lt;strong&gt;~67 KB&lt;/strong&gt; (0.17% of total archive size).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overhead to extract a single file inside it:&lt;/strong&gt; &lt;strong&gt;97 bytes&lt;/strong&gt; (local header + entry data).&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Why this is possible
&lt;/h3&gt;

&lt;p&gt;The ZIP format specification places the table of contents (called the &lt;strong&gt;Central Directory&lt;/strong&gt;) at the &lt;strong&gt;very end&lt;/strong&gt; of the file, not at the beginning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ local header + file data ] ... [ Central Directory ] [ EOCD (22 bytes) ]
                                 ^                     ^
                                 Table of contents     End of Central Directory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last record in the file is the &lt;strong&gt;End of Central Directory (EOCD)&lt;/strong&gt;. It is 22 bytes long and contains:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Total number of entries in the ZIP&lt;/li&gt;
&lt;li&gt;Byte size of the Central Directory&lt;/li&gt;
&lt;li&gt;Byte offset where the Central Directory starts&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;(Note: A ZIP can have an optional trailing comment up to 65,535 bytes, so the EOCD is located somewhere within the last &lt;code&gt;22 + 65,535 = 65,557&lt;/code&gt; bytes).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If the remote web server supports HTTP Range requests (&lt;code&gt;Accept-Ranges: bytes&lt;/code&gt;), we can request just the tail of the file.&lt;/p&gt;




&lt;h3&gt;
  
  
  How it works step-by-step
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Step 1: Fetch the tail (EOCD) with a Range request
&lt;/h4&gt;

&lt;p&gt;Send an HTTP Range request for the end of the file. Most archives have no comment, so a 4 KB read is usually enough. If no EOCD signature is found, fall back to 65,557 bytes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Request the last 4096 bytes first&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FIRST_TAIL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LONGEST_TAIL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mh"&gt;0xffff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 65557 bytes&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;FIRST_TAIL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;tail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;findEOCD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// scans backward for signature 0x06054b50&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Trailing comment present: fetch the full 65k tail&lt;/span&gt;
  &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;LONGEST_TAIL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;tail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;findEOCD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note: Always scan backwards for &lt;code&gt;0x06054b50&lt;/code&gt; because that 4-byte sequence could randomly exist inside compressed payload bytes.&lt;/em&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: Read the Central Directory
&lt;/h4&gt;

&lt;p&gt;From the 22-byte EOCD record, pull the count, size, and offset:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DataView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;byteOffset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUint16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cdSize&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cdOffset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then send a second range request for &lt;code&gt;cdOffset&lt;/code&gt; to &lt;code&gt;cdOffset + cdSize&lt;/code&gt; (or slice it directly from memory if it was already included in the tail you fetched):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Each record starts with 0x02014b50&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;46&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;cd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mh"&gt;0x02014b50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;flags&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUint16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nameLength&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUint16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x0800&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf-8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;latin1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                       &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;46&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;nameLength&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;packedSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;uncompressedSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;46&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;nameLength&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUint16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUint16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you the full file tree, file sizes, and offsets.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 3: Extract a single file on demand
&lt;/h4&gt;

&lt;p&gt;When the user clicks on a specific file:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fetch the 30-byte local header at &lt;code&gt;entry.offset&lt;/code&gt; to determine the start of the compressed data.&lt;/li&gt;
&lt;li&gt;Fetch only the compressed byte range (&lt;code&gt;entry.packedSize&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Decompress in the browser using the native Streams API (no external libraries needed):
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;packedBytes&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipeThrough&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DecompressionStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deflate-raw&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fileBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Quick test with curl (Try it yourself)
&lt;/h3&gt;

&lt;p&gt;You can verify this in your terminal on any remote ZIP:&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="c"&gt;# 1. Check if the server supports ranges (look for Accept-Ranges: bytes)&lt;/span&gt;
curl &lt;span class="nt"&gt;-sI&lt;/span&gt; https://example.com/large.zip | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; accept-ranges

&lt;span class="c"&gt;# 2. Download only the last 65 KB of the archive&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-65557&lt;/span&gt; https://example.com/large.zip &lt;span class="nt"&gt;-o&lt;/span&gt; tail.bin

&lt;span class="c"&gt;# 3. unzip will read the central directory directly from the tail&lt;/span&gt;
unzip &lt;span class="nt"&gt;-l&lt;/span&gt; tail.bin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Edge cases and limitations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Small files (&amp;lt; 100 KB):&lt;/strong&gt; Two HTTP round trips to fetch 64 KB of an 80 KB archive take longer than a single GET request for the whole file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Servers ignoring Range headers:&lt;/strong&gt; Some servers or CDNs do not honor byte ranges and respond with &lt;code&gt;200 OK&lt;/code&gt; sending the whole file anyway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ZIP64:&lt;/strong&gt; Archives over 4 GB or with more than 65,535 files store offsets in a separate &lt;code&gt;PK\x06\x06&lt;/code&gt; record.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tarballs (&lt;code&gt;.tar.gz&lt;/code&gt;):&lt;/strong&gt; Does not work on tar.gz archives because gzip streams do not have an index at the end and must be decoded sequentially from byte 0.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  About the project
&lt;/h3&gt;

&lt;p&gt;I implemented this technique in &lt;strong&gt;&lt;a href="https://foretaste.page" rel="noopener noreferrer"&gt;Foretaste&lt;/a&gt;&lt;/strong&gt;, a lightweight browser extension that lets you right-click any file link to inspect ZIP contents, read documents (PDF, DOCX, XLSX), or view media directly in your browser without downloading.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>programming</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
