{"record":{"id":"3bd7f4c495c0a164","repo":"BookStackApp/BookStack","slug":"pdf-export-via-command-failed-unable-to-read-pdf","errorCode":null,"errorMessage":"PDF Export via command failed, unable to read PDF output file","messagePattern":"PDF Export via command failed, unable to read PDF output file","errorType":"exception","errorClass":"PdfExportException","httpStatus":null,"severity":"error","filePath":"app/Exports/PdfGenerator.php","lineNumber":172,"sourceCode":"        };\n\n        try {\n            $process->run();\n        } catch (ProcessTimedOutException $e) {\n            $cleanup();\n            throw new PdfExportException(\"PDF Export via command failed due to timeout at {$timeout} second(s)\");\n        }\n\n        if (!$process->isSuccessful()) {\n            $cleanup();\n            throw new PdfExportException(\"PDF Export via command failed with exit code {$process->getExitCode()}, stdout: {$process->getOutput()}, stderr: {$process->getErrorOutput()}\");\n        }\n\n        $pdfContents = file_get_contents($outputPdf);\n        $cleanup();\n\n        if ($pdfContents === false) {\n            throw new PdfExportException(\"PDF Export via command failed, unable to read PDF output file\");\n        } else if (empty($pdfContents)) {\n            throw new PdfExportException(\"PDF Export via command failed, PDF output file is empty\");\n        }\n\n        return $pdfContents;\n    }\n\n    protected function renderUsingWkhtml(string $html): string\n    {\n        $snappy = new SnappyPdf($this->getWkhtmlBinaryPath());\n        $options = config('exports.snappy.options');\n        return $snappy->getOutputFromHtml($html, $options);\n    }\n\n    /**\n     * Taken from https://github.com/barryvdh/laravel-dompdf/blob/v2.1.1/src/PDF.php\n     * Copyright (c) 2021 barryvdh, MIT License\n     * https://github.com/barryvdh/laravel-dompdf/blob/v2.1.1/LICENSE","sourceCodeStart":154,"sourceCodeEnd":190,"githubUrl":"https://github.com/BookStackApp/BookStack/blob/18f8469a1c72f8cc8497e9372635e6dea5028071/app/Exports/PdfGenerator.php#L154-L190","documentation":"Thrown by renderUsingCommand when file_get_contents() on the temporary output PDF path returns false after the external command reported success. The library cannot read back the PDF it expects the command to have written to {output_pdf_path}, so the export fails. Usually the command exited 0 without actually writing the output file (or wrote it elsewhere / deleted it).","triggerScenarios":"Calling fromHtml() with exports.pdf_command configured; the command exits 0 but never writes to the {output_pdf_path} temp file — e.g. the command ignores the placeholder, writes to a hard-coded path, is a wrapper script that swallows failures, or the temp file was removed by an external cleaner between write and read.","commonSituations":"Misconfigured pdf_command whose argument order means {output_pdf_path} is not the output destination (e.g. a tool where flags come after the input); a command that prints the PDF to stdout instead of writing the file; tmpwatch/systemd-tmpfiles purging sys_get_temp_dir(); custom scripts exiting 0 regardless of the child's result.","solutions":["Check that exports.pdf_command actually writes its result to the {output_pdf_path} argument and that argument order is correct for the chosen tool.","Test the command manually: create two temp files, run the command with their paths substituted, and confirm the output file exists and is non-empty.","If your tool writes to stdout instead of a file, wrap it (sh -c 'tool in > out') so the placeholder receives the bytes.","Ensure no tmp cleaner daemon is removing files from the system temp dir mid-request; point TMPDIR to a stable writable directory if needed.","If the command is a custom wrapper, make it propagate child exit codes (exit $?) so failures are caught as exit-code errors instead of silently succeeding."],"exampleFix":"// before (.env) — tool prints PDF to stdout, never fills {output_pdf_path}\nPDF_COMMAND=/usr/bin/pandoc {input_html_path} -o pdf\n\n// after\nPDF_COMMAND=/bin/sh -c 'pandoc \"$0\" -o \"$1\"' {input_html_path} {output_pdf_path}","handlingStrategy":"validation","validationCode":"<?php\n// verify the command actually produces a non-empty output file before production use\n$in = tempnam(sys_get_temp_dir(), 'chk-in');\n$out = tempnam(sys_get_temp_dir(), 'chk-out');\nfile_put_contents($in, '<h1>test</h1>');\n$cmd = str_replace(\n    ['{input_html_path}', '{output_pdf_path}'],\n    [escapeshellarg($in), escapeshellarg($out)],\n    config('exports.pdf_command')\n);\nshell_exec($cmd);\n$isReady = file_exists($out) && filesize($out) > 0;\n@unlink($in); @unlink($out);","typeGuard":null,"tryCatchPattern":"try {\n    $pdf = $pdfGenerator->fromHtml($html);\n} catch (\\BookStack\\Exceptions\\PdfExportException $e) {\n    if (str_contains($e->getMessage(), 'unable to read PDF output file')) {\n        // command exited 0 but wrote nothing; audit exports.pdf_command placeholder usage\n        Log::error('PDF output missing despite success exit code', ['cmd' => config('exports.pdf_command')]);\n    }\n    throw $e;\n}","preventionTips":["Ensure the configured command's output destination is exactly {output_pdf_path}, in the correct argument position.","Wrap stdout-producing tools with a shell redirect so bytes land in the placeholder file.","In wrapper scripts, propagate the child exit code so silent failures surface as exit-code errors.","Run a one-shot end-to-end command smoke test on deploy to confirm a non-empty PDF is produced."],"tags":["php","filesystem","pdf-export","shell-command","symfony-process"],"backgroundTag":"output-file-not-written","analyzedSha":"18f8469a1c72f8cc8497e9372635e6dea5028071","analyzedAt":"2026-09-02T19:49:33.068Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T02:17:09.455Z"}