Skip to content

Bug: iOS Safari throws NotReadableError "The I/O read operation failed" after downloading Blob URL #7

Description

@jeremy-code

Consider the following code:

import { ErrorBoundary } from "react-error-boundary";

import { memo, Suspense, use, useMemo, useState } from "react";

const saveFile = (file: File) => {
  const blobUrl = URL.createObjectURL(file);
  const anchorElement = document.createElement("a");
  anchorElement.href = blobUrl;
  anchorElement.download = file.name;
  document.body.appendChild(anchorElement);
  anchorElement.click();
  document.body.removeChild(anchorElement);
  URL.revokeObjectURL(blobUrl);
};

// Without memo, Safari iOS either just vaguely says "A problem repeatedly occurred on..."
// even if saveFile isn't used
const FileBytes = memo(
  ({ bytesPromise }: { bytesPromise: Promise<Uint8Array> }) => {
    const bytes = use(bytesPromise);

    return <div>{Array.from(bytes).slice(0, 20)}</div>;
  },
);

function App() {
  const [file, setFile] = useState<File | null>(null);
  const bytesPromise = useMemo(
    () => (file !== null ? file.bytes() : null),
    [file],
  );

  return (
    <>
      <input
        type="file"
        onChange={(e) => {
          if (e.target.files !== null) {
            const file = Array.from(e.target.files)[0];

            if (file !== undefined) {
              setFile(file);
            }
          }
        }}
      />
      <button
        // Errors both when onClick is async and sync
        onClick={async () => {
          if (file !== null) {
            // Pretend I have changed the file in some way
            const newFile = new File([file], file.name, {
              type: file.type,
              lastModified: file.lastModified,
            });
            setFile(newFile);
            saveFile(newFile);
          }
        }}
      >
        Save file
      </button>

      {file !== null && (
        <div>
          {file.name}
          {file.type}
        </div>
      )}

      {bytesPromise !== null && (
        <ErrorBoundary
          fallbackRender={({ error }) => (
            <div>
              {error instanceof Error
                ? JSON.stringify({
                    name: error.name,
                    message: error.message,
                    stack: error.stack,
                    cause: error.cause,
                  })
                : "Unknown error"}
            </div>
          )}
        >
          <Suspense fallback={<>Loading...</>}>
            <FileBytes bytesPromise={bytesPromise} />
          </Suspense>
        </ErrorBoundary>
      )}
    </>
  );
}

export default App;

When it runs, on mobile iOS Safari (v. 26.3), it will throw a NotReadableError error with message "The I/O read operation failed." This error does not occur on Firefox, Chrome, or Desktop Safari.

The crux of the error surprisingly is the saveFile call. If you remove it, everything will work perfectly. I am not certain of the reason why.

At first, I assumed the error was due to saveFile somehow invalidating the original Blob, but even with various clone implementations, it still would error. This does include sync implementations using just the new File constructor and/or slice.

// errors
const [newFile, newFile2] = await Promise.all([cloneFile(file), cloneFile(file)];
setFile(newFile);
saveFile(newFile2);

// errors
const newFile = await cloneFile(file);
saveFile(await cloneFile(file));
setFile(newFile);

// works
const newFile = await cloneFile(file);
setFile(newFile);
saveFile(await cloneFile(file));

Given the latter worked, it may seem that setting the file before cloning it again to be saved is the solution but after some testing, I noticed more bizarre behavior in that the following worked.

const newFile = new File([file], file.name, {
  type: file.type,
  lastModified: file.lastModified,
});
setFile(newFile);
void (await file.bytes());
saveFile(newFile);

I used bytes here but text and arrayBuffer method do work (but stream and slice do not). Given the useless return, it may seem that the problem involves the click handler being asynchronous but the following will also error:

<button
  onClick={() => {
    if (file !== null) {
      // The I/O read operation failed. name NotReadableError

      const newFile = new File([file], file.name, {
        type: file.type,
        lastModified: file.lastModified,
      });
      setFile(newFile);
      saveFile(newFile);
    }
  }}
>
  Save file
</button>

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions