{"record":{"id":"aab273af205bf107","repo":"garrytan/gstack","slug":"pdftotext-failed-on-pdfpath-err-message","errorCode":null,"errorMessage":"pdftotext failed on ${pdfPath}: ${err.message}","messagePattern":"pdftotext failed on (.+?): (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"make-pdf/src/pdftotext.ts","lineNumber":193,"sourceCode":"/**\n * Run pdftotext on a PDF and return the extracted text.\n *\n * Uses `-layout` by default because that's what downstream normalization\n * expects. Callers that need raw text can pass layout=false.\n */\nexport function pdftotext(pdfPath: string, opts?: { layout?: boolean }): string {\n  const info = resolvePdftotext();\n  const layout = opts?.layout ?? true;\n  const args: string[] = [];\n  if (layout) args.push(\"-layout\");\n  args.push(pdfPath, \"-\");   // \"-\" = stdout\n  try {\n    return execFileSync(info.bin, args, {\n      encoding: \"utf8\",\n      maxBuffer: 32 * 1024 * 1024,\n    });\n  } catch (err: any) {\n    throw new Error(`pdftotext failed on ${pdfPath}: ${err.message}`);\n  }\n}\n\n/**\n * Normalize extracted text for cross-platform, cross-flavor diffing.\n *\n * What we strip / normalize:\n *   - Unicode: NFC canonical composition (macOS emits NFD; Linux emits NFC;\n *     this dodges the fundamental encoding diff).\n *   - CR and CRLF → LF (Windows Xpdf emits CRLF).\n *   - Form feeds (\\f) → double newline (Poppler emits \\f at page breaks).\n *   - Trailing spaces on every line.\n *   - Runs of 3+ blank lines → 2 blank lines.\n *   - Leading/trailing whitespace on the whole string.\n *   - Non-breaking space (U+00A0) → regular space.\n *   - Zero-width space (U+200B) and zero-width non-joiner (U+200C) → empty.\n *   - Soft hyphen (U+00AD) → empty (pdftotext -layout sometimes emits these\n *     for hyphens: auto breaks).","sourceCodeStart":175,"sourceCodeEnd":211,"githubUrl":"https://github.com/garrytan/gstack/blob/94993f74012782fd94416dd44b8314f6363a13a4/make-pdf/src/pdftotext.ts#L175-L211","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\ntry {\n  return execFileSync(info.bin, args, { encoding:'utf8', maxBuffer: 32*1024*1024 });\n} catch (err: any) {\n  throw new Error(`pdftotext failed on ${pdfPath}: ${err.message}`);\n}\n\n// after: surface exit code, signal, and stderr distinctly\ntry {\n  return execFileSync(info.bin, args, { encoding:'utf8', maxBuffer: 64*1024*1024 });\n} catch (err: any) {\n  if (err.code === 'ENOENT') throw new Error(`pdftotext binary missing: ${info.bin}`);\n  const stderr = err.stderr?.toString().trim() ?? '';\n  throw new Error(`pdftotext failed on ${pdfPath} (exit ${err.status}, signal ${err.signal ?? 'none'}): ${stderr || err.message}`);\n}","handlingStrategy":"try-catch","validationCode":"import fs from 'node:fs';\n\nfunction preflightPdf(pdfPath: string): string | null {\n  if (!fs.existsSync(pdfPath)) return `pdf not found: ${pdfPath}`;\n  const head = Buffer.alloc(5);\n  const fd = fs.openSync(pdfPath, 'r');\n  try { fs.readSync(fd, head, 0, 5, 0); } finally { fs.closeSync(fd); }\n  if (head.toString() !== '%PDF-') return `not a PDF: ${pdfPath}`;\n  return null;\n}","typeGuard":null,"tryCatchPattern":"try {\n  return pdftotext(pdfPath);\n} catch (e) {\n  const msg = String((e as Error).message);\n  if (/encrypt/i.test(msg)) {\n    // skip encrypted PDFs in the gate\n    return '';\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["make-pdf","pdftotext","subprocess","execfilesync","pdf"],"backgroundTag":null,"analyzedSha":"94993f74012782fd94416dd44b8314f6363a13a4","analyzedAt":"2026-08-12T04:06:23.140Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}