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
- Run `pdftotext -layout <pdf> -` manually to see poppler's real error.
- If the PDF is encrypted, provide the password or regenerate it without encryption.
- Increase maxBuffer if the PDF legitimately produces >32MB of text.
- 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
- Generate PDFs without encryption for the copy-paste gate fixtures.
- Pre-validate the %PDF magic bytes before invoking pdftotext.
- Increase maxBuffer if legitimate test PDFs exceed 32MB of text.
- Pin the poppler version in CI to avoid feature-rejection regressions.
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
- browse ${args[0] || "unknown"} exited ${exitCode}: ${stderr}
- pdftotext not found. make-pdf needs pdftotext to run the co
- pdf: --from-file requires a path
- Unknown pdf flag: ${a}
- pdf: --margins is mutex with --margin-top/--margin-right/--m
AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12).
Data as JSON: /api/errors/aab273af205bf107.
Report an issue: GitHub.