Engineering Journal
Pdf Processor
Pdf Processor

The ArrayBuffer Detach Bug: Why Your Cached Worker Bytes Go Empty After the First PDF

2026-05-31

TLDR

Passing typed arrays (Uint8Array) into Web Worker APIs like pdfjsLib.getDocument({ data: bytes }) triggers zero-copy ArrayBuffer transfers. Under the JavaScript specification, transferred ArrayBuffer instances are immediately neutered (byteLength === 0). If worker code caches the original Uint8Array reference for subsequent operations (such as single-page re-extraction), subsequent reads fail silently. Slicing the array (bytes.slice()) prior to transfer preserves cached byte data.
Buffer StateCaching ApproachPost-Transfer byteLengthRe-Extraction Result
Direct Reference_cachedBytes = bytes0 (Neutered / Detached)Silent failure (Worker hangs)
Sliced Independent Copy_cachedBytes = bytes.slice()Original Byte Length100% Deterministic Re-Extraction

Problem statement: the silent re-extraction hang

In our PDF processor UI, clicking "Re-extract Page" after modifying tolerance sliders produced no response from the Web Worker.

No console errors were logged, no message returned to the main thread, and the re-extraction UI button remained in a loading state indefinitely.


Technical failure mode: ArrayBuffer neutering

PDF.js transfers input ArrayBuffer objects to sub-workers using zero-copy transfers:

// DEFECTIVE WORKER CACHING: Storing reference prior to transfer
let _cachedPdfBytes = null;

self.onmessage = async (e) => { const { bytes } = e.data; _cachedPdfBytes = bytes; // Caches direct reference to Uint8Array

// PDF.js transfers the underlying ArrayBuffer, neutering _cachedPdfBytes! const pdfDoc = await pdfjsLib.getDocument({ data: bytes }).promise; };

When single-page re-extraction was triggered later:

// FAILS: _cachedPdfBytes.byteLength is now 0!
const pdfDoc = await pdfjsLib.getDocument({ data: _cachedPdfBytes }).promise;

PDF.js received an empty 0-byte buffer. The internal rejection was caught inside a generic try/catch block that lacked routing for re-processing errors, causing a silent hang.


The fix & architecture: pre-transfer buffer copying

Slice bytes to create an independent ArrayBuffer copy prior to invoking pdfjsLib.getDocument():

// REFACTORED WORKER: Slice before transfer
let _cachedPdfBytes = null;

self.onmessage = async (e) => { const { bytes } = e.data;

// 1. Create an independent copy BEFORE transfer neuters the buffer _cachedPdfBytes = bytes.slice();

// 2. Pass original bytes to PDF.js const pdfDoc = await pdfjsLib.getDocument({ data: bytes }).promise; };

Defense-in-Depth guard verification

Add length verification and explicit error handling prior to reprocessing:
async function handleReprocessPageRequest(pageNum) {
  if (!_cachedPdfBytes || _cachedPdfBytes.byteLength === 0) {
    self.postMessage({
      type: 'error',
      isReprocess: true,
      error: 'Cached PDF bytes are missing or neutered. Re-run initial document extraction.'
    });
    return;
  }

// Safe to execute re-extraction using valid cached bytes const pdfDoc = await pdfjsLib.getDocument({ data: _cachedPdfBytes }).promise; const page = await pdfDoc.getPage(pageNum); // ... execute single-page pipeline ... }

Rule of thumb: Always execute .slice() on Uint8Array data before passing buffers to Web Worker APIs that use zero-copy Transferable objects.
Read this post in the full Engineering Journal →