How to Fix Odoo wkhtmltopdf PDF Generation Errors
Fix Odoo PDF errors by checking wkhtmltopdf builds, report.url, asset access, proxy settings, and large-report limits.
Direct answer: Most Odoo PDF failures come from an unsupported wkhtmltopdf build, a renderer that cannot reach Odoo assets, or a report that exceeds wkhtmltopdf resource limits. Check the binary first, compare the HTML and PDF routes, then fix report.url, proxy access, report assets, and document size in that order.
1. Confirm the wkhtmltopdf build
Odoo renders QWeb reports as HTML and delegates PDF creation to wkhtmltopdf. A correct browser preview does not prove that the PDF process can load the same CSS, fonts, images, or JavaScript.
which wkhtmltopdf
wkhtmltopdf --version
wkhtmltopdf --extended-help | head -n 20
Run these commands as the same Unix user or container service account that runs Odoo. Odoo’s compatibility guidance recommends 0.12.5-1 for Odoo 10–15 and 0.12.6.1-3 for Odoo 16 and later. Distribution packages may omit the patched Qt changes required for headers and footers.
# systemd
sudo -u odoo wkhtmltopdf --version
# Docker or Podman
podman exec -it odoo wkhtmltopdf --version
# Kubernetes
kubectl exec deploy/odoo -- wkhtmltopdf --version
A different binary, PATH, certificate store, or font set inside the Odoo container can produce a different result from your interactive shell.
2. Compare the HTML and PDF routes
Open both routes for the same record. Odoo documents these endpoints in its QWeb reports documentation.
https://your-odoo.example.com/report/html/your_module.report_template/42
https://your-odoo.example.com/report/pdf/your_module.report_template/42
- HTML is also wrong: fix the QWeb template, external layout, data, CSS, or asset bundle.
- HTML is right but PDF is wrong: inspect wkhtmltopdf, URL reachability, certificates, and asset responses.
- Both redirect to login: inspect proxy rules, cookies, and the authentication context available to report rendering.
3. Fix missing CSS, logos, fonts, and images
When styles or logos disappear while HTML looks correct, wkhtmltopdf usually cannot download linked assets from Odoo. Odoo builds those links from web.base.url. Behind a reverse proxy, set the dedicated report.url parameter to an address reachable from the Odoo server, such as an internal service name and port.
- Enable developer mode and open Settings → Technical → Parameters → System Parameters.
- Set
report.url, for examplehttp://odoo:8069inside a container network orhttp://127.0.0.1:8069on a shared host. - Keep the public
web.base.urlunless changing it is intentional. - If proxy or login behavior keeps rewriting the base URL, set
web.base.url.freezetoTrue.
curl -I http://odoo:8069/web/assets/your_asset_bundle.css
curl -I http://odoo:8069/web/image/your_model/42/image_1920
getent hosts odoo
curl -vk https://internal-odoo.example.com/web/assets/your_asset_bundle.css
Look for refused connections, DNS failures, 404 or 403 responses, certificate errors, and redirects. A browser on your laptop may succeed while the Odoo process cannot route to the same hostname.
Proxy and container checklist
- Attach Odoo to the network where the renderer runs.
- Allow the internal hostname and port through firewall rules.
- Pass forwarded host and protocol consistently to avoid HTTP/HTTPS redirect loops.
- Install the required CA chain when internal HTTPS uses a private certificate.
- Ensure asset routes do not require browser-only headers or an expired session cookie.
4. Repair QWeb and report assets
- Include custom fonts in the report asset bundle.
- Call the intended external layout so company headers, footers, and paper settings are applied.
- Use absolute or Odoo-generated image URLs instead of developer-machine filesystem paths.
- Keep critical CSS in the report bundle. JavaScript-dependent layout may not finish before capture.
- Regenerate assets after XML or SCSS changes and clear stale asset attachments when required by your deployment.
curl -L -b cookies.txt -o report.html \
'https://your-odoo.example.com/report/html/your_module.report_template/42'
rg -n 'href=|src=|@font-face|external_layout' report.html
5. Diagnose common errors
Error code -8, -11, or another non-zero exit
Capture the complete Odoo log line and wkhtmltopdf version. Check for an unsupported build, unreachable assets, exhausted memory, malformed HTML, and oversized tables. Error codes alone do not identify the root cause.
Headers and footers are missing
This commonly indicates a build without patched Qt. Install the Odoo-compatible build for your release, verify it as the Odoo service account, and then check that header and footer templates are reachable through report.url.
The logo is missing but text appears
Request the image URL from inside the Odoo runtime. Fix DNS, proxy authentication, certificate trust, permissions, or a 404 asset route. Do not change the public base URL unless that is intentional.
The PDF is blank or incomplete
Check for timeouts, JavaScript that never settles, redirects to login, and renderer crashes. Reduce the report to one record, remove optional sections, and compare a short document with the failing one.
6. Handle long and complex reports
- Reproduce with a small page range to find the section that triggers the crash.
- Reduce nested tables, wide cells, huge inline images, and repeated header markup.
- Split large batches into smaller PDFs and merge them afterward if your workflow permits.
- Monitor process RSS, open file descriptors, temporary disk space, and container memory limits.
- Increase limits only after measuring. Removing headers and footers can be a diagnostic workaround.
Odoo’s compatibility wiki describes memory, file-descriptor, and table-layout problems on documents of roughly 500 or more pages. A third-party module such as fix_wkhtmltopdf claims to address buffer-overflow and -8 failures for large PDFs when headers and footers are not required. Treat it as version-specific, review it, and validate it in staging.
7. Repeatable troubleshooting runbook
- Record the Odoo version, OS or container image, wkhtmltopdf version, and report action.
- Run
wkhtmltopdf --versionas the Odoo service account and replace an unpatched or mismatched build. - Compare
/report/html/...and/report/pdf/...for one record. - Set an internally reachable
report.url; freezeweb.base.urlif proxy rewriting persists. - Test CSS, image, font, and JavaScript endpoints from the renderer environment.
- Read Odoo and proxy logs during one reproduction and fix the first failed request.
- Retest with a minimal report, then add sections until the failing asset or layout is isolated.
- Load-test representative page counts while monitoring memory, file descriptors, and temporary storage.
8. Or skip the browser setup
For screenshots of report previews, invoices, dashboards, or any web page, ScreenshotNeo provides a single API request. It accepts consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the verdict and billing. Its MCP server lets Claude, Cursor, and other MCP clients call screenshot tools directly.
See the ScreenshotNeo API docs for full-page or selector capture, custom CSS and JavaScript, waits, headers, cookies, user agents, blocking rules, PDF output, caching, async jobs, bulk capture, and signed links.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://your-odoo.example.com/report/html/your_module.report_template/42 -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://your-odoo.example.com/report/html/your_module.report_template/42"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://your-odoo.example.com/report/html/your_module.report_template/42' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
The free plan includes 1,000 shots per month with no card. Paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Create a free ScreenshotNeo account.
9. Performance, reliability, and cost notes
- Performance: Reuse a compatible renderer image, keep reports asset-light, and split very large batches. ScreenshotNeo caching can reduce repeated capture work when you choose a TTL.
- Reliability: Treat asset reachability as a deployment dependency. Add checks for internal DNS, certificates, proxy routes, and temporary storage.
- Cost: wkhtmltopdf consumes Odoo CPU and memory even when a PDF fails. ScreenshotNeo bills only clean shots; failed loads, blank pages, bot checks, timeouts, and cache hits cost nothing.
10. FAQ
Why does the HTML route work while the PDF route fails?
The browser has network access or session state that wkhtmltopdf lacks, or the binary has renderer limitations. Test from the Odoo runtime and inspect logs.
Should I change web.base.url to the internal hostname?
Usually set report.url for report rendering and leave the public web.base.url unchanged. Freeze it only when automatic changes cause redirects or instability.
Can CSS fixes solve a missing header or footer?
Verify a patched-Qt build first, then verify the header or footer template and its assets.
When should I replace wkhtmltopdf?
Replace it when the build is unsupported, cannot provide required headers or footers, or repeatedly fails after network and template issues are fixed.


