Engineering Journal
Pdf Processor
Pdf Processor

ArrayBuffer Detach: Why Reprocess Threw Empty PDF on Every Call

2026-06-01

TLDR

Invoking PDF.js parsing methods (pdfjsLib.getDocument({ data: bytes })) causes zero-copy ArrayBuffer transfers to internal Web Worker threads, immediately detaching the array buffer in the calling context. Storing a direct reference to bytes causes subsequent single-page reprocessing requests (triggered from region editors) to fail with "Empty PDF" errors due to byteLength === 0. Executing bytes.slice() prior to transfer guarantees data availability.
Caching StrategyPointer TypePost-Transfer byteLengthReprocessing Call Status
Direct Reference_cachedBytes = bytes0 (Detached / Neutered)Fails ("Empty PDF" error)
Sliced Buffer Copy_cachedBytes = bytes.slice()Original Byte Length100% Deterministic Reprocessing

Problem statement: the region editor reprocess crash

When users edited bounding boxes on the region editor canvas and clicked "Reprocess Page", the worker thread crashed or threw an unhandled internal exception: Error: Empty PDF.

Debugging revealed that while initial document loading succeeded, _cachedBytes.byteLength evaluated to 0 inside the worker thread prior to reprocessing.


Technical failure mode: ArrayBuffer transfer semantics

Passing Uint8Array data into PDF.js triggers structured clone transfer semantics:

// DEFECTIVE IMPLEMENTATION: Storing reference prior to transfer
let _cachedBytes = null;

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

// getDocument transfers bytes.buffer, setting byteLength to 0! const pdf = await pdfjsLib.getDocument({ data: bytes }).promise; };

When the region editor subsequently triggered a reprocess pass:

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

PDF.js received an empty 0-byte buffer and threw an internal parsing exception.


The fix: pre-transfer independent buffer cloning

Copy the byte array using .slice() before passing the original buffer to PDF.js:

// REFACTORED WORKER: Slice array before transfer neuters buffer
let _cachedBytes = null;

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

// 1. Create independent ArrayBuffer copy BEFORE transfer _cachedBytes = bytes.slice();

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

Safety guard implementation

Validate cached buffer integrity before executing reprocessing passes:
export async function executeRegionReprocess(pageNum, overrides) {
  if (!_cachedBytes || _cachedBytes.byteLength === 0) {
    throw new Error('Cached PDF byte buffer is neutered or empty. Re-initialize document.');
  }

// Safe to parse cached bytes const pdf = await pdfjsLib.getDocument({ data: _cachedBytes }).promise; const page = await pdf.getPage(pageNum); return reprocessPageWithOverrides(page, overrides); }

Rule of thumb: Always clone typed byte arrays (bytes.slice()) before passing them to libraries that execute zero-copy Web Worker transfers.
Read this post in the full Engineering Journal →