Skip to content

[Bug]: Unhandled rejection when a document that failed to load after a network error is destroyed #22366

Closed

Description

@andrewfader

Attach (recommended) or Link to PDF file

network_error.pdf, network_error_2.pdf

These come from real production incidents. The original PDFs are confidential, so these are anonymized copies (all content replaced, structure kept). The PDFs themselves are fine. The bug is triggered by the connection dropping while one of them is loading.

Web browser and its version

Chrome 156.0.8078.47

Operating system and its version

macOS 27.0

PDF.js version

v6.4.299 (latest release), and master at 24a63a4 (also the online demo, v6.5.410)

Is the bug present in the latest PDF.js version?

Yes

Is a browser extension

No

Steps to reproduce the problem

  1. Save the script below as repro.mjs. It serves the PDF, drops the connection halfway through the response body, and loads it with the released pdf.js from jsDelivr.
  2. node repro.mjs drop network_error.pdf
  3. Open http://localhost:8000/ and the console.
// Usage: node repro.mjs <mode> <file.pdf>, then open http://localhost:8000/
// Modes: whole-file  (range requests not starting at 0 get a 200 with the whole file)
//        other-range (range requests are answered with a 206 for the preceding range)
//        drop        (the connection drops halfway through the response body)
import fs from "node:fs";
import http from "node:http";

const [mode, file] = process.argv.slice(2);
const data = fs.readFileSync(file);
const PDFJS = "https://cdn.jsdelivr.net/npm/pdfjs-dist@6.4.299/build";

const page = `<!doctype html><pre id="log"></pre><script type="module">
const log = msg => { document.getElementById("log").textContent += msg + "\\n"; console.log(msg); };
addEventListener("unhandledrejection", e => log("Uncaught (in promise): " + e.reason));
const pdfjsLib = await import("${PDFJS}/pdf.min.mjs");
pdfjsLib.GlobalWorkerOptions.workerSrc = "${PDFJS}/pdf.worker.min.mjs";
const task = pdfjsLib.getDocument({ url: "/file.pdf", disableAutoFetch: true, disableStream: true });
try {
  const doc = await task.promise;
  for (let i = 1; i <= doc.numPages; i++) await (await doc.getPage(i)).getOperatorList();
  log("Loaded " + doc.numPages + " pages");
} catch (e) {
  log("Failed: " + e);
}
await task.destroy();
setTimeout(() => log("done"), 500);
</script>`;

http.createServer((req, res) => {
  if (req.url !== "/file.pdf") {
    res.writeHead(200, { "Content-Type": "text/html" }).end(page);
    return;
  }
  const headers = { "Content-Type": "application/pdf", "Accept-Ranges": "bytes" };
  const range = /bytes=(\d+)-(\d+)/.exec(req.headers.range ?? "");
  if (mode === "drop") {
    res.writeHead(200, { ...headers, "Content-Length": data.length });
    res.write(data.subarray(0, data.length >> 1), () => res.destroy());
  } else if (!range || (mode === "whole-file" && range[1] !== "0")) {
    res.writeHead(200, { ...headers, "Content-Length": data.length }).end(data);
  } else {
    const [start, end] = [+range[1], +range[2] + 1];
    // other-range: the same length of data, but from the preceding range
    const begin = mode === "other-range" ? Math.max(0, 2 * start - end) : start;
    res.writeHead(206, {
      ...headers,
      "Content-Range": `bytes ${begin}-${begin + end - start - 1}/${data.length}`,
      "Content-Length": end - start,
    });
    res.end(data.subarray(begin, begin + end - start));
  }
}).listen(8000, () => console.log(`${mode}: open http://localhost:8000/`));

What is the expected behavior?

getDocument rejects with the network error, and destroying the loading task afterwards (e.g. the viewer is closed) is silent.

What went wrong?

After the document fails to load, loadingTask.destroy() cancels the errored stream reader. ReadableStreamDefaultReader.cancel() then returns a promise rejected with the stream's error, which nothing handles:

Failed: UnknownErrorException: network error
Uncaught (in promise): TypeError: network error

We hit this in production twice, from users whose connection dropped while a PDF was loading. It was reported as an unhandled rejection from PDFFetchStreamReader.cancel.

JavaScript console:

Failed to load resource: net::ERR_CONTENT_LENGTH_MISMATCH
Uncaught (in promise) TypeError: network error

Additional context

The same this._reader.cancel(reason) without a .catch is in PDFFetchStreamReader, PDFFetchStreamRangeReader, the PDFWorkerStream readers, and the PDFNodeStream readers. Similar to #22171. I have a fix with unit tests that fail on master and pass with it. The fix is in #22371.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions