How to Fix Google Apps Script HTML-to-PDF Conversion Failures
Trace Apps Script PDF failures from template evaluation to conversion, authorization, HTTP responses and quotas, with fixes and runnable code.
Most Google Apps Script HTML-to-PDF failures happen before the PDF conversion call. Treat the workflow as three separate stages: evaluate the template, convert the resulting HtmlOutput or supported Blob, then save or send the bytes. Log each stage and inspect the actual response when UrlFetchApp is involved.
1. Identify the failing stage
Start with a minimal diagnostic wrapper. It tells you whether the problem is template code, HTML creation, conversion, or storage.
function debugPdfPipeline() {
let htmlOutput;
try {
const template = HtmlService.createTemplateFromFile('Invoice');
Logger.log('Template loaded');
htmlOutput = template.evaluate();
Logger.log('Template evaluated; content length: %s', htmlOutput.getContent().length);
} catch (error) {
Logger.log('STAGE template/evaluation failed: %s', error.stack || error);
throw error;
}
let pdfBlob;
try {
pdfBlob = htmlOutput.getAs('application/pdf').setName('invoice.pdf');
Logger.log('STAGE conversion succeeded; bytes: %s', pdfBlob.getBytes().length);
} catch (error) {
Logger.log('STAGE conversion failed: %s', error.stack || error);
throw error;
}
try {
const file = DriveApp.createFile(pdfBlob);
Logger.log('STAGE storage succeeded; file ID: %s', file.getId());
} catch (error) {
Logger.log('STAGE storage failed: %s', error.stack || error);
throw error;
}
}
Read the execution log and fix the first failing stage. A filename ending in .pdf does not prove that the bytes are a valid PDF.
2. Evaluate an HTML template before converting it
Apps Script templates contain server-side scriptlets. createTemplateFromFile() returns an HtmlTemplate; evaluate() executes the scriptlets and creates the HtmlOutput that can be converted. Browser JavaScript that would run after a page loads is a different mechanism and is not executed by template evaluation. See the official templated HTML guide.
function createInvoicePdf() {
const template = HtmlService.createTemplateFromFile('Invoice');
template.customer = {
name: 'Ada Lovelace',
total: '$125.00'
};
const htmlOutput = template.evaluate()
.setTitle('Invoice');
const pdfBlob = htmlOutput
.getAs('application/pdf')
.setName('invoice.pdf');
return DriveApp.createFile(pdfBlob).getUrl();
}
The HtmlOutput.getAs() reference documents conversion to a blob in the requested content type and adding the appropriate extension.
Inspect generated server code
If evaluation throws a syntax or runtime error, inspect the generated code. The template guide documents both methods and says line correspondence is preserved for errors in evaluated template code.
function inspectInvoiceTemplate() {
const template = HtmlService.createTemplateFromFile('Invoice');
Logger.log(template.getCode());
Logger.log(template.getCodeWithComments());
}
Look for unclosed scriptlet tags, JavaScript syntax errors, undefined server-side variables, and values containing quotes or unexpected types.
3. Build valid HTML before asking for a PDF
If you do not need scriptlets, create an HtmlOutput directly and inspect its content. createHtmlOutput can fail on malformed input, so do not treat the PDF call as the only possible fault point.
function htmlStringToPdf() {
const html = 'Report
Generated by Apps Script.
';
const output = HtmlService.createHtmlOutput(html);
Logger.log(output.getContent());
return output.getAs('application/pdf').setName('report.pdf');
}
Keep CSS self-contained while debugging. External assets, browser-only APIs and client-side code can make the rendered result differ from what you see in a normal browser.
4. Use the correct conversion method
| Input | Use | Typical mistake |
|---|---|---|
| Evaluated HTML template | HtmlOutput.getAs('application/pdf') |
Calling conversion on the unevaluated HtmlTemplate |
| Plain HTML string | HtmlService.createHtmlOutput(html), then getAs() |
Passing malformed HTML or undefined values |
| Existing service blob | Blob.getAs('application/pdf') only when the source type is supported |
Assuming any blob named .pdf is convertible |
The Blob.getAs() documentation describes conversion from supported source types. A renamed file is not a conversion.
5. Debug UrlFetchApp export responses
When a workflow fetches a PDF export URL, inspect the HTTP response before saving it. UrlFetchApp requires the https://www.googleapis.com/auth/script.external_request scope. With muteHttpExceptions: true, Apps Script returns an HTTPResponse even for an error status, allowing you to log the status and body.
function fetchPdfAndSave() {
const url = 'https://docs.google.com/spreadsheets/d/SPREADSHEET_ID/export?format=pdf';
const response = UrlFetchApp.fetch(url, {
method: 'get',
headers: { Authorization: 'Bearer ' + ScriptApp.getOAuthToken() },
muteHttpExceptions: true
});
const code = response.getResponseCode();
const contentType = response.getHeaders()['Content-Type'] || '';
const body = response.getContentText();
Logger.log('HTTP %s; Content-Type: %s; first bytes: %s', code, contentType, body.slice(0, 200));
if (code < 200 || code >= 300) {
throw new Error('PDF export failed with HTTP ' + code + ': ' + body.slice(0, 500));
}
if (!contentType.toLowerCase().includes('pdf')) {
throw new Error('Expected PDF but received ' + contentType);
}
const blob = response.getBlob().setName('sheet-export.pdf');
return DriveApp.createFile(blob).getId();
}
An authentication page, JSON error, or HTML error document can otherwise be stored with a .pdf name and fail later in email, preview, or downstream processing.
Google’s Generate and send PDFs from Google Sheets sample shows the spreadsheet-specific export pattern. It is designed for reports represented in Sheets, not as a general HTML renderer.
6. Sheets export versus HtmlOutput conversion
| Question | HtmlOutput conversion | Sheets export |
|---|---|---|
| Best input | HTML assembled or evaluated by Apps Script | A report laid out in a Google Sheets template |
| Conversion call | htmlOutput.getAs('application/pdf') |
Fetch the spreadsheet /export URL |
| Primary checks | Template evaluation, HTML validity, conversion quota | Spreadsheet authorization, URL Fetch scope, HTTP status and content |
| Scope of official example | HtmlOutput API conversion | Google’s spreadsheet PDF automation sample |
Choose Sheets export when the document is naturally tabular and already lives in a spreadsheet. Do not switch to it simply because arbitrary HTML conversion failed; first isolate the failing stage.
7. Quotas, runtime and batch reliability
- Check the current Apps Script quotas page for your account. Conversion and URL Fetch limits are account-dependent and can change.
- Google warns that newly created Workspace domains may temporarily have stricter conversion quotas.
- Apps Script executions have runtime limits; split large batches into resumable jobs and record completed items.
- Reuse static HTML and CSS where possible, and avoid repeatedly fetching the same remote resources.
- For each output, record stage, URL or record ID, response code, content type, byte count and exception text.
- Retry transient fetch failures with bounded exponential backoff, but do not retry deterministic template syntax errors.
function withBackoff(work, attempts) {
let lastError;
for (let i = 0; i < attempts; i++) {
try {
return work();
} catch (error) {
lastError = error;
if (i === attempts - 1) break;
Utilities.sleep(Math.pow(2, i) * 1000);
}
}
throw lastError;
}
8. Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
Cannot find function getAs on a template |
You converted an HtmlTemplate instead of evaluated output |
Call evaluate(), then getAs('application/pdf'). |
| Template syntax or undefined-variable error | Server-side scriptlet failure | Use getCode()/getCodeWithComments(); check scriptlets and assigned variables. |
| Conversion quota or service exception | Account or domain conversion limit | Check current quotas, reduce batch size, and retry later. |
| PDF opens as HTML or JSON | HTTP request returned an error page | Use muteHttpExceptions; inspect status, headers and first bytes before saving. |
Exception: Request failed for ... UrlFetchApp |
Missing authorization, blocked endpoint or non-2xx response | Authorize the external request scope and log the response with exceptions muted. |
| Blank or incomplete output | Dynamic content was never available to server-side rendering | Move required values into the evaluated template; do not rely on browser-side JavaScript. |
| Batch stops near the end | Execution runtime or service quota | Checkpoint progress, resume from the last item and keep batches smaller. |
| Drive save fails after conversion | Drive authorization or invalid blob state | Log byte count, reauthorize Drive, and save only after conversion succeeds. |
9. A complete, defensive invoice example
function generateInvoicePdf() {
const data = {
customer: 'Ada Lovelace',
items: [
{ description: 'Consulting', quantity: 2, price: 50 },
{ description: 'Support', quantity: 1, price: 25 }
]
};
const template = HtmlService.createTemplateFromFile('Invoice');
template.data = data;
const output = template.evaluate().setTitle('Invoice');
const content = output.getContent();
if (!content || content.length < 20) throw new Error('Evaluated HTML is empty');
const pdf = output.getAs('application/pdf').setName('invoice.pdf');
if (pdf.getBytes().length === 0) throw new Error('Converted PDF is empty');
const file = DriveApp.createFile(pdf);
return { id: file.getId(), url: file.getUrl(), bytes: pdf.getBytes().length };
}
In Invoice.html, escape values according to their context and keep the document structure valid:
<!doctype html>
<html>
<head><meta charset="utf-8"><style>body{font-family:Arial;margin:32px}</style></head>
<body>
<h1>Invoice</h1>
<p><?= data.customer ?></p>
<? data.items.forEach(function(item) { ?>
<p><?= item.description ?> — <?= item.quantity ?> × $<?= item.price ?></p>
<? }); ?>
</body>
</html>
10. Or skip the browser setup
If your actual requirement is a reliable screenshot or PDF capture of a URL rather than an Apps Script-generated document, ScreenshotNeo provides a website capture API and MCP server. One request can capture a clean page while removing cookie banners, newsletter popups and chat widgets before the shot. Bot checks, blank pages, failed loads, timeouts and cache hits are not billed, and responses identify the verdict and billing status.
See the ScreenshotNeo API documentation for the available options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
- Cookie banners, popups and chat widgets are removed before the shot.
- Bot checks, blank pages and failed loads are never billed.
- An MCP server lets AI agents use
take_screenshot,get_page_infoandcapture_pdf. - 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000.
Create a free ScreenshotNeo account.
11. FAQ
Does evaluate() run JavaScript in the browser?
No. It executes Apps Script server-side scriptlets and returns an HtmlOutput. Browser-side behavior is separate.
Should every PDF workflow use Sheets export?
No. Use Sheets export for spreadsheet-shaped reports; use HtmlOutput.getAs() for evaluated HTML.
Why can a saved “PDF” be unreadable?
The source may be an HTML or JSON error response. Validate HTTP status, content type and bytes before saving.
Where can I find current quota values?
Use Google's current quotas documentation; limits are account-dependent and may change.
Can I retry every conversion error?
Retry transient service or network failures with a bounded backoff. Fix template syntax, authorization and invalid-input errors before retrying.


