DEV Community

Foretaste
Foretaste

Posted on Originally published at foretaste.page

Why Browsers Force File Downloads and How to Preview Binary Data Client-Side

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.

You click the link. Instead of opening cleanly in a new browser tab, the browser triggers an immediate download to your hard drive.

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 .sqlite, .parquet, and .docx files.

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.


Why Browsers Force Downloads Instead of Showing Files

Browsers do not download files by accident. They download them when one of three web standards tells them to:

1. The Content-Disposition Header

Web servers often send this header in their HTTP responses:

Content-Disposition: attachment; filename="report-q3.xlsx"
Enter fullscreen mode Exit fullscreen mode

The keyword attachment 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.

2. The HTML5 Download Attribute

If an HTML link has the download attribute:

<a href="/data/analytics.parquet" download>Download Dataset</a>
Enter fullscreen mode Exit fullscreen mode

The browser obeys the author's directive and routes the incoming stream directly to the local disk.

3. Generic or Missing MIME Types

If an S3 bucket or API endpoint sends a file with Content-Type: application/octet-stream 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.


The Hidden Security and Privacy Risks

Forced downloads are not just an annoyance. They create real security and privacy risks.

The SVG Script Execution Trap

Most people treat SVG files like ordinary images such as PNGs or JPEGs. But SVGs are XML documents, and XML allows embedded <script> tags, inline event handlers like <rect onload="...">, and external resource fetches.

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).

To render an SVG safely, it must be isolated inside a sandboxed iframe with scripts explicitly disabled:

<iframe sandbox="" srcdoc="...sanitized SVG markup..."></iframe>
Enter fullscreen mode Exit fullscreen mode

Magic Bytes vs. File Extension Spoofing

Relying on a file extension in a URL is unsafe. An attacker can name a link financial_report.pdf while serving a Windows binary or a malicious script.

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:

async function detectRealMimeType(url: string): Promise<string> {
  const headBytes = await fetchByteRange(url, 0, 512);

  // Check PDF signature: %PDF- (0x25 0x50 0x44 0x46 0x2D)
  if (startsWith(headBytes, [0x25, 0x50, 0x44, 0x46, 0x2d])) {
    return 'application/pdf';
  }

  // Check SQLite format 3 signature: 'SQLite format 3\0'
  if (startsWith(headBytes, [0x53, 0x51, 0x4c, 0x69, 0x74, 0x65, 0x20, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x20, 0x33, 0x00])) {
    return 'application/x-sqlite3';
  }

  // Check Parquet magic bytes: 'PAR1' (0x50 0x41 0x52 0x31)
  if (startsWith(headBytes, [0x50, 0x41, 0x52, 0x31])) {
    return 'application/vnd.apache.parquet';
  }

  // Check ZIP / Office container: 'PK\x03\x04'
  if (startsWith(headBytes, [0x50, 0x4b, 0x03, 0x04])) {
    return 'application/zip';
  }

  return 'application/octet-stream';
}
Enter fullscreen mode Exit fullscreen mode

The Third-Party Cloud Viewer Danger

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".

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.

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


Peeking Into Complex Formats Locally

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.

1. Parquet Datasets: Reading the Footer

Apache Parquet is the standard format for big data analytics. A single Parquet file can easily be 500 MB or 2 GB.

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 PAR1. Immediately preceding those 4 bytes is a 4-byte little-endian integer indicating the byte length of the metadata footer.

[ Data Page 1 ]
[ Data Page 2 ]
...
[ FileMetaData (Thrift encoded) ]
[ 4-byte Footer Length ]
[ 4-byte Magic 'PAR1' ]
Enter fullscreen mode Exit fullscreen mode

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:

async function readParquetFooter(url: string, fileSize: number) {
  // 1. Fetch the last 8 bytes to get the footer length
  const tail = await fetchByteRange(url, fileSize - 8, fileSize);
  const view = new DataView(tail.buffer, tail.byteOffset, 8);
  const footerLen = view.getUint32(0, true);

  // 2. Fetch only the metadata footer bytes
  const footerBytes = await fetchByteRange(url, fileSize - 4 - footerLen, fileSize - 4);

  // 3. Decode Thrift metadata in browser memory
  return decodeParquetMetadata(footerBytes);
}
Enter fullscreen mode Exit fullscreen mode

You can view column names, data types, and row group counts instantly, without transferring the underlying gigabytes of column data.

2. SQLite Databases: Paging Tables Without Full Downloads

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 SELECT queries or inspect the schema.

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 sqlite_schema table.

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.

3. Paging Giant CSV Files

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.

The correct approach is streaming with offset indexing:

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

Row 500,000 costs the exact same memory and rendering time as row 1.


How to Handle This in Real Workflows

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

I built this into Foretaste, a browser extension available for Chrome, Edge, and Firefox.

Here is how it works:

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

Conclusion

The web was originally built to let us read and browse documents, not force us into managing local filesystem clutter.

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.

Top comments (0)