Receiving an empty string from Chrome FileReader when dealing with large files (300MB or more)

Objective:

  • The task is to read a file from the user's file system as a base64 string in the browser
  • The size of these files can be up to 1.5GB

Challenge:

  • A script that works flawlessly on Firefox, regardless of the file size
  • On Chrome, the script performs well with smaller files (tested with files around 5MB)
  • However, when selecting a larger file (e.g., 400MB), FileReader completes without errors or exceptions but returns an empty string instead of the expected base64 string

Queries:

  • Is this a bug specific to Chrome?
  • Why does it not generate any errors or exceptions despite the issue?
  • What are the potential solutions or workarounds for this problem?

Note:

It's crucial to highlight that chunking is not feasible as the full base64 string needs to be sent via 'POST' to an API that does not support chunks.

Code:

'use strict';

var filePickerElement = document.getElementById('filepicker');

filePickerElement.onchange = (event) => {
  const selectedFile = event.target.files[0];
  console.log('selectedFile', selectedFile);

  readFile(selectedFile);
};

function readFile(selectedFile) {
  console.log('START READING FILE');
  const reader = new FileReader();

  reader.onload = (e) => {
    const fileBase64 = reader.result.toString();

    console.log('ONLOAD','base64', fileBase64);
    
    if (fileBase64 === '') {
      alert('Result string is EMPTY :(');
    } else {
        alert('It worked as expected :)');
    }
  };

  reader.onprogress = (e) => {
    console.log('Progress', ~~((e.loaded / e.total) * 100 ), '%');
  };

  reader.onerror = (err) => {
    console.error('Error reading the file.', err);
  };

  reader.readAsDataURL(selectedFile);
}
<!doctype html>
<html lang="en">

<head>
  <!-- Required meta tags -->
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <!-- Bootstrap CSS -->
  <link href="https://cdn.jsdelivr.net/npm/<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="6d0f0202191e191f0c1d2d58435d435d">[email protected]</a>/dist/css/bootstrap.min.css" rel="stylesheet"
    integrity="sha384-wEmeIV1mKuiNpC+IOBjI7aAzPcEZeedi5yW5f2yOq55WWLwNGmvvx4Um1vskeMj0" crossorigin="anonymous">

  <title>FileReader Issue Example</title>
</head>

<body>

  <div class="container">
    <h1>FileReader Issue Example</h1>
    <div class="card">
      <div class="card-header">
        Select File:
      </div>
      <div class="card-body">
        <input type="file" id="filepicker" />
      </div>
    </div>

  </div>

  <script src="https://cdn.jsdelivr.net/npm/<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="67050808131413150617275249574957">[email protected]</a>/dist/js/bootstrap.bundle.min.js"
    integrity="sha384-p34f1UUtsS3wqzfto5wAAmdvj+osOnFyQFpp4Ua3gs/ZVWx6oOypYoCJhGGScy+8"
    crossorigin="anonymous"></script>
  <script src="main.js"></script>
</body>

</html>

Answer №1

Is this a chrome bug?

In response to the query posed in Chrome, FileReader API, event.target.result === "", it is important to note that this limitation is not a bug, but rather an intentional constraint within V8 (the JavaScript engine used by Chrome and other platforms). The issue stems from the inability to construct a String exceeding 512MB on 64-bit systems due to V8's heap object limitations, as explained in this commit.

Why is there neither an error nor an exception?

While creating such a large string directly does result in a RangeError, certain operations lack the expected error handling mechanisms. As outlined in FileReader::readOperation Step 3, exceptions should trigger specific actions which are absent in this case.

Detailed steps involving Uint32Array and Blob further illustrate this anomaly, indicating a potential oversight that warrants attention and correction.

I will pursue this matter by raising a relevant issue to address the absence of error handling within the FileReader interface.

How can I fix or work around this issue?

A recommended approach involves modifying your API endpoint to accept binary resources directly instead of relying on data:// URLs, the usage of which is generally discouraged.

An alternative solution for future implementation would entail sending a ReadableStream to your endpoint and performing the data:// URL conversion autonomously using a stream sourced from the Blob.

(Code snippet omitted)

For immediate resolution, consider storing chunks of base64 representations in a Blob, although caution is advised due to potential server-side constraints related to handling excessively large strings. Communication with the API maintainer is strongly suggested to mitigate any issues arising from V8's inherent limitations.

Answer №2

Here is a clever method for converting a blob into chunks and then transforming them into base64 blobs. These base64 chunks are concatenated within a JSON blob along with some pre/suffix JSON parts.

By keeping it as a blob, the browser can efficiently manage memory allocation and even offload it to disk if necessary.

If you adjust the chunkSize to be larger, the browser prefers to store smaller blob chunks in memory (in one bucket).

// Obtaining a sample gradient file (blob)
var canvas=document.createElement("canvas"), context=canvas.getContext("2d"), gradient=context.createLinearGradient(0,0,3000,3000);canvas.width=canvas.height=3000;gradient.addColorStop(0,"red");gradient.addColorStop(1,"blue");context.fillStyle=gradient;context.fillRect(0,0,canvas.width,canvas.height);canvas.toBlob(main);

async function main (blob) {
  var fileReader = new FileReader()
  // It's recommended to add 2 so it omits == from all but the last chunk
  var chunkSize = (1 << 16) + 2 
  var position = 0
  var b64Chunks = []
  
  while (position < blob.size) {
    await new Promise(resolve => {
      fileReader.readAsDataURL(blob.slice(position, position + chunkSize))
      fileReader.onload = () => {
        const base64 = fileReader.result.split(',')[1]
        b64Chunks.push(new Blob([base64]))
        resolve()
      }
      position += chunkSize
    })
  }

  // How you combine all chunks into json is now up to you.
  // This solution just outlines what needs to be done
  // There are more automated ways, but here is a simple form
  // (just a heads-up: this new blob won't create a lot of data in memory, it will only reference other blob locations)
  const jsonData = new Blob([
    '{"data": "', ...b64Chunks, '"}'
  ], { type: 'application/json' })

  /*
  // It's strongly recommended to ask API developers 
  // to implement support for binary/file uploads (multipart-formdata)
  // Base64 is roughly ~33% bigger and handling streaming 
  // this data on the server to disk is nearly impossible 
  fetch('./upload-files-to-bad-json-only-api', {
    method: 'POST',
    body: jsonData
  })
  */
  
  // Just testing that it still functions
  //
  // new Response(jsonData).json().then(console.log)
  fetch('data:image/png;base64,' + await new Blob(b64Chunks).text()).then(response => response.blob()).then(blob => console.log(URL.createObjectURL(blob)))
}

I opted not to use

base64 += fileReader.result.split(',')[1]
and JSON.stringify since dealing with GiB of data is substantial and JSON isn't ideal for handling binary data.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What steps do I need to take in order to create functions that are

I have 10 functions with similar structures: function socialMedia_ajax(media){ return ajaxRequest('search/' + media + 'id', media + 'id').then( function(res) { var imgStatus = res[1].length > 0 ? "c ...

What is the best way to save a string for future use in Angular after receiving it from a POST request API?

I have been assigned to a project involving javascript/typescript/angular, even though I have limited experience with these technologies. As a result, please bear with me as I may lack some knowledge in this area. In the scenario where a user logs in, ther ...

How can you make a button in Vue only visible after all API calls have successfully retrieved the data?

Is there a way to make the report button visible only after all the data has been loaded from the latest click of the budget button? Currently, when the budget button is clicked, it retrieves budgets and displays them in a Vue grid using Kendo Grid. To spe ...

Is it illegal to escape quotes when using an image source attribute and onerror event in HTML: `<img src="x" onerror="alert("hello")" />`?

Experimenting with escape characters has been a fascinating experience for me. <img src="x" onerror=alert('hello'); /> <img src="x" onerror="alert(\"hello\")" /> The second code snippet triggers an illegal character error ...

Having trouble retrieving the table value from an HTML document?

I am trying to retrieve specific information from this source: This information is crucial for fetching data from a database using a primary key. However, extracting this value has proven to be quite challenging. Upon document readiness, I execute the fol ...

Having trouble getting Three.js JSON models to cast shadows properly?

Recently, I've been experimenting with blender exported models in Three.js. I have successfully imported a model and observed how light interacts with the object. While a directionalLight is illuminating the front-facing parts, I'm facing an issu ...

Javascript issue: SyntaxError - A numerical value is required after the decimal point

I am currently in the process of setting up an HTML form to trigger an AJAX update when a user exits a field. My current attempt is focusing on one table cell and it looks like this: <td><input type="text" class="form-control" id="firstName" name ...

Implementing interactive dropdown menus to trigger specific actions

I have modified some code I found in a tutorial on creating hoverable dropdowns from W3. Instead of the default behavior where clicking on a link takes you to another page, I want to pass a value to a function when a user clicks. Below is a snippet of the ...

Endless loop caused by Angular UI-router's promise resolving

I'm attempting to retrieve data from my SQLite database before loading a view using resolve when defining the state in ui-router. Currently, this is how I've set up the state: .state("menu", { templateUrl: "templates/menu.html", control ...

Display additional inputs using the PHP Foreach Loop depending on the selection made

I have a PHP Foreach loop that includes a "Quantity" input field. When users select a quantity, the corresponding number of new inputs should be displayed. For example, if the user chooses a quantity of "3", then 3 new inputs should appear for that item. K ...

Tips for transferring an array variable into a div element using AJAX

I have a div like the following: <div id="#myid"> if($values){ echo "<p>$values['one']</p>"; echo "<p>$values['two']</p>"; } </div> Due to the large size of my div, I want to make a request ...

Dealing with Request Disconnection in a Node.js and Express Application

I have a node/express application and I am looking for a way to detect unexpected interruptions in the connection. I have attempted using the following code: req.on('close', function () { //this handles browser/tab closure scenarios }) Howev ...

D3.js Issue: The <g> element's transform attribute is expecting a number, but instead received "translate(NaN,NaN)"

I encountered an error message in my console while attempting to create a data visualization using d3. The specific error is as follows: Error: <g> attribute transform: Expected number, "translate(NaN,NaN)". To build this visualization, I ...

Crafting artistic shapes using the Canny Edge Detection technique on Canvas

Can someone provide some guidance on using Canny Edge Detection to generate shapes in Canvas? ...

The ajax function nestled within a nested forEach loop completes its execution once the inner callback function is triggered

I am encountering a problem with an ajax call that is nested within a forEach loop inside another function. The issue is that the callback of the outer function is being triggered before the inner loop completes, resulting in "staticItemList" not being p ...

Keep a close eye on your Vue app to make sure it's

<template> <v-form :model='agency'> <v-layout row wrap> <v-flex xs12 sm12 lg12 > <v-layout row wrap> <v-flex xs12 md12 class="add-col-padding-right"> <v-radio-group v-mod ...

Activate fancybox when clicked, triggering numerous ajax requests

Although I achieved my desired output, the method in which it was done is not ideal because AJAX duplicates with every click. Check out my code: <a href="/messages/schedule" class="greenbtn fancybox">Schedule</a> $('a.fancybox').cl ...

What are the advantages of choosing express.js over Ruby on Sinatra?

Currently brainstorming for a social app and contemplating the switch from my initial option, Sinatra/Ruby to express.js/nodejs. My main focus is on the abundance of open source projects in Ruby that can expedite development. Another major consideration i ...

Deleting a row from a table in AngularJS can be accomplished by following these steps

I am having trouble with deleting rows from a table using angularjs. When I try to delete a row, it ends up deleting the previous row instead of the correct one. How can I fix this issue? Please check out the working DEMO Here is the code snippet: < ...

The HTTP request is being executed twice for some reason unknown to me

import React, {useState, useEffect} from 'react' export function UseStateExample() { // This is a named export that must be used consistently with {} when importing/exporting. const [resourceType, setResourceType] = useState(null) useEffect ...