garrytan/gstack · error · Error

pdftotext failed on ${pdfPath}: ${err.message}

Error message

pdftotext failed on ${pdfPath}: ${err.message}

What it means

Thrown when execFileSync of pdftotext on a PDF fails — pdftotext was resolved successfully (so 337 does not apply) but the actual extraction run exited non-zero or could not complete. The message wraps err.message, which typically carries Node's spawn error string including stderr. Used by the copy-paste CI gate to extract text for diffing.

Source

Thrown at make-pdf/src/pdftotext.ts:193

/**
 * Run pdftotext on a PDF and return the extracted text.
 *
 * Uses `-layout` by default because that's what downstream normalization
 * expects. Callers that need raw text can pass layout=false.
 */
export function pdftotext(pdfPath: string, opts?: { layout?: boolean }): string {
  const info = resolvePdftotext();
  const layout = opts?.layout ?? true;
  const args: string[] = [];
  if (layout) args.push("-layout");
  args.push(pdfPath, "-");   // "-" = stdout
  try {
    return execFileSync(info.bin, args, {
      encoding: "utf8",
      maxBuffer: 32 * 1024 * 1024,
    });
  } catch (err: any) {
    throw new Error(`pdftotext failed on ${pdfPath}: ${err.message}`);
  }
}

/**
 * Normalize extracted text for cross-platform, cross-flavor diffing.
 *
 * What we strip / normalize:
 *   - Unicode: NFC canonical composition (macOS emits NFD; Linux emits NFC;
 *     this dodges the fundamental encoding diff).
 *   - CR and CRLF → LF (Windows Xpdf emits CRLF).
 *   - Form feeds (\f) → double newline (Poppler emits \f at page breaks).
 *   - Trailing spaces on every line.
 *   - Runs of 3+ blank lines → 2 blank lines.
 *   - Leading/trailing whitespace on the whole string.
 *   - Non-breaking space (U+00A0) → regular space.
 *   - Zero-width space (U+200B) and zero-width non-joiner (U+200C) → empty.
 *   - Soft hyphen (U+00AD) → empty (pdftotext -layout sometimes emits these
 *     for hyphens: auto breaks).

View on GitHub (pinned to 94993f7401)

Solutions

  1. Run `pdftotext -layout <pdf> -` manually to see poppler's real error.
  2. If the PDF is encrypted, provide the password or regenerate it without encryption.
  3. Increase maxBuffer if the PDF legitimately produces >32MB of text.
  4. Reinstall/repair poppler if the binary itself is broken.

Example fix

// before
try {
  return execFileSync(info.bin, args, { encoding:'utf8', maxBuffer: 32*1024*1024 });
} catch (err: any) {
  throw new Error(`pdftotext failed on ${pdfPath}: ${err.message}`);
}

// after: surface exit code, signal, and stderr distinctly
try {
  return execFileSync(info.bin, args, { encoding:'utf8', maxBuffer: 64*1024*1024 });
} catch (err: any) {
  if (err.code === 'ENOENT') throw new Error(`pdftotext binary missing: ${info.bin}`);
  const stderr = err.stderr?.toString().trim() ?? '';
  throw new Error(`pdftotext failed on ${pdfPath} (exit ${err.status}, signal ${err.signal ?? 'none'}): ${stderr || err.message}`);
}
Defensive patterns

Strategy: try-catch

Validate before calling

import fs from 'node:fs';

function preflightPdf(pdfPath: string): string | null {
  if (!fs.existsSync(pdfPath)) return `pdf not found: ${pdfPath}`;
  const head = Buffer.alloc(5);
  const fd = fs.openSync(pdfPath, 'r');
  try { fs.readSync(fd, head, 0, 5, 0); } finally { fs.closeSync(fd); }
  if (head.toString() !== '%PDF-') return `not a PDF: ${pdfPath}`;
  return null;
}

Try / catch

try {
  return pdftotext(pdfPath);
} catch (e) {
  const msg = String((e as Error).message);
  if (/encrypt/i.test(msg)) {
    // skip encrypted PDFs in the gate
    return '';
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling pdftotext(pdfPath) where the resolved pdftotext binary fails on the given PDF: corrupt/encrypted PDF, ENOENT on the binary (deleted after resolution), maxBuffer (32MB) overflow on a huge PDF, or a non-zero exit from poppler on an unsupported PDF version.

Common situations: A CI run on a malformed PDF fixture; an encrypted PDF without a password; a PDF larger than 32MB of extractable text; pdftotext binary deleted/moved between resolution and execution; a poppler version that rejects a new PDF feature.

Related errors


AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12). Data as JSON: /api/errors/aab273af205bf107. Report an issue: GitHub.