Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

An “empty PDF” usually means your code read binary bytes as text or JSON, received an opaque CORS response, or saved an error page with a .pdf extension. Check the response status and headers first, then consume a successful PDF with response.blob() or response.arrayBuffer(). In Axios, set responseType explicitly.

Start with the response, not the file extension

Open your browser’s DevTools, select the request in Network, and inspect the final response after redirects and any preflight request. Record:

  • HTTP status and response.ok
  • Content-Type
  • Content-Length, when the browser can expose it
  • the downloaded byte count
  • the response type, especially whether it is opaque

A status of 200 only means the server completed an HTTP request. It does not prove that the body is a PDF. A response with application/json may be an authentication or validation error, while text/html is often a login page, proxy error, or application exception. Read those bodies as text while debugging instead of writing them to report.pdf.

For a byte-level check, read an ArrayBuffer and inspect its first few bytes. A normal PDF normally begins with the ASCII signature %PDF. A first character of {, <, or an error message indicates that the endpoint returned JSON, HTML, or plain text instead. This signature check is useful triage, not proof that every PDF structure is valid.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use Fetch’s binary readers correctly

Do not call response.text() or response.json() on a successful PDF body. Those methods decode the stream into another representation and can corrupt or discard binary data. Fetch provides blob() for browser downloads and arrayBuffer() when you need to inspect, parse, forward, or store the raw bytes.

Browser download with validation

const response = await fetch('/api/report');

if (!response.ok) {
  const message = await response.text();
  throw new Error(`HTTP ${response.status}: ${message}`);
}

const type = response.headers.get('content-type') || '';
if (!type.toLowerCase().includes('application/pdf')) {
  const body = await response.text();
  throw new Error(`Expected PDF, received ${type || 'no Content-Type'}: ${body.slice(0, 300)}`);
}

const blob = await response.blob();
if (blob.size === 0) throw new Error('PDF body is empty');

const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'report.pdf';
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(url);

Check the content type before consuming the body because a response body can normally be read only once. If you need both diagnostics and bytes, clone the response before reading it, or read an ArrayBuffer once and derive your diagnostics from that buffer.

ArrayBuffer for parsers and byte sinks

const response = await fetch('/api/report');
if (!response.ok) throw new Error(`HTTP ${response.status}`);

const bytes = await response.arrayBuffer();
if (bytes.byteLength === 0) throw new Error('Server returned zero bytes');

const header = new TextDecoder().decode(bytes.slice(0, 5));
if (header !== '%PDF-') {
  throw new Error(`Unexpected body signature: ${JSON.stringify(header)}`);
}

// Pass bytes to a PDF parser, upload stream, or file-writing API.

Fix Axios responses that look like an empty object

Axios needs an explicit binary response type. In a browser, use blob; for Node.js or raw-byte processing, use arraybuffer. Without that setting, binary data may be transformed according to default rules. Axios issue reports describe the confusing symptom of response.data appearing as an empty object; treat that as a binary-handling failure to investigate, not as evidence that the PDF itself is empty.

Browser Axios download

const { data, headers, status } = await axios.get('/api/report', {
  responseType: 'blob',
  headers: { Accept: 'application/pdf' }
});

if (status < 200 || status >= 300) {
  throw new Error(`HTTP ${status}`);
}
if (!data || data.size === 0) throw new Error('Empty PDF body');

const blob = data.type
  ? data
  : new Blob([data], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'report.pdf';
link.click();
URL.revokeObjectURL(url);

Node.js Axios bytes

const response = await axios.get(PDF_URL, {
  responseType: 'arraybuffer',
  headers: { Accept: 'application/pdf' },
  validateStatus: () => true
});

const contentType = response.headers['content-type'] || '';
if (response.status < 200 || response.status >= 300) {
  const message = Buffer.from(response.data).toString('utf8');
  throw new Error(`HTTP ${response.status}: ${message}`);
}
if (!contentType.toLowerCase().includes('application/pdf')) {
  throw new Error(`Expected PDF, received ${contentType}`);
}
if (response.data.byteLength === 0) throw new Error('Empty PDF body');
require('node:fs').writeFileSync('report.pdf', Buffer.from(response.data));

Opaque CORS responses explain zero-byte Blobs

An opaque response is produced when the browser is not allowed to expose a cross-origin response to your script. Its status is 0, headers are inaccessible, and its body is effectively unavailable. Calling blob() on it produces a Blob with size 0 and an empty type, so URL.createObjectURL() cannot produce a useful PDF URL.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Confirm that the API permits your requesting origin and that your request’s mode and credentials match the server’s CORS policy. Do not “fix” this by blindly changing to mode: 'no-cors'; that intentionally gives JavaScript an opaque response. Test the endpoint directly, or call it through a same-origin backend proxy. A proxy also keeps API credentials out of browser code.

Make the server forward PDF bytes and headers

Your server-side route must send the generated bytes unchanged. Set Content-Type: application/pdf. Set Content-Disposition when you want a download filename; inline displays the document in a capable browser, while attachment prompts a download.

Express-style proxy

const upstream = await fetch(PDF_URL, options);

if (!upstream.ok) {
  const error = await upstream.text();
  return res.status(upstream.status).type('text').send(error);
}

const bytes = await upstream.arrayBuffer();
if (bytes.byteLength === 0) {
  return res.status(502).json({ error: 'Upstream returned an empty PDF' });
}

res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', 'attachment; filename=document.pdf');
res.send(Buffer.from(bytes));

Do not call res.json(), JSON.stringify(), or a text encoding function on the PDF buffer. Preserve an upstream error status and body while debugging so the client can distinguish generation failure from download failure.

Next.js route considerations

Read the upstream response with arrayBuffer(), return that ArrayBuffer from the route, and attach the same two headers. Avoid middleware that parses or rewrites the body. If a framework has a response-size limit, raise it or stream the file for large documents.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Fetch versus Axios and browser versus proxy

Decision What to verify Typical choice
Body reader PDF is binary, not text or JSON Fetch blob()/arrayBuffer(); Axios blob/arraybuffer
Runtime Whether you need a browser download or raw bytes Blob for browser UI; ArrayBuffer for Node, parsers, and forwarding
Cross-origin call CORS headers, credentials, and preflight behavior Same-origin backend proxy when policy or secrets are restrictive
Error handling Error bodies may be JSON or HTML Check status and content type before saving
URL cleanup Object URLs consume browser resources Call URL.revokeObjectURL() after starting the download

A direct browser call is simple when the API intentionally supports your origin and uses a short-lived client-safe token. A backend proxy gives you credential control, server-side byte validation, centralized logging, and consistent handling of non-PDF errors. Never expose a secret API key in client-side code.

Performance, reliability, and cost checks

  • Use one body read and avoid converting the entire document to a base64 string; base64 increases memory use.
  • Set a client timeout and cancel abandoned requests with AbortController.
  • Log status, content type, byte count, request ID, and generation time without logging private document contents.
  • Validate the upstream body before returning it. A fast 200 response containing an HTML error is still a failed PDF job.
  • For large or slow documents, prefer a server-side job that stores the result and returns a download URL, with an expiry policy.
  • Do not retry every 4xx response. Retry transient network failures and selected 5xx responses with bounded exponential backoff, while ensuring generation is idempotent.

Common symptoms and precise fixes

“Network shows 200, but the file is blank”

Inspect Content-Type and the byte count. If the type is JSON or HTML, fix authentication, parameters, redirects, or server generation. If it is PDF but the count is zero, inspect the generator and proxy for an empty buffer.

“Blob.size is 0 and Blob.type is empty”

Check for an opaque CORS response. A status of 0 and inaccessible headers confirm that JavaScript cannot read the body. Configure CORS or use a same-origin proxy.

“Axios data is an empty object”

Set responseType: 'blob' in a browser or 'arraybuffer' in Node.js. Do not pass the result through JSON serialization.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

“The downloaded PDF contains an error message”

You saved a non-PDF response with a PDF extension. Check status and content type, read the error body as text or JSON, and fix the upstream request before writing the file.

“The PDF opens but has no pages”

The transport succeeded, so investigate document generation: empty input data, a failed template render, missing fonts or assets, and page-range settings. Compare the generated byte count and PDF signature at the service boundary.

“It works in a script but not in the browser”

The browser enforces CORS and exposes fewer headers. Check preflight requests, allowed origins, credentials, and whether a redirect lands on a different origin. Move the request behind your own backend when you cannot change the PDF service.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If your goal is to obtain a PDF or image representation of a webpage rather than debug an existing PDF endpoint, ScreenshotNeo provides a single HTTP API and an MCP server for AI agents. Its PDF capture supports paper size, margins, landscape mode, and page ranges; the service can also wait for selectors, delays, or network idle before capture.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

For a direct request, see the ScreenshotNeo documentation. The supplied API call is:

curl -G "https://api.screenshotneo.com/v1/shot" 
  -d access_key=YOUR_API_KEY 
  --data-urlencode url=https://stripe.com 
  -o shot.webp

ScreenshotNeo accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result. Its MCP tools—take_screenshot, get_page_info, and capture_pdf—let Claude, Cursor, or another MCP client request captures. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

Final verification checklist

  1. Confirm the final response status is successful.
  2. Confirm Content-Type is application/pdf.
  3. Read the body as a Blob or ArrayBuffer, never as text or JSON.
  4. Reject a zero-byte body before creating a download link.
  5. Check the first bytes for %PDF- during diagnosis.
  6. Resolve opaque CORS responses through server policy or a proxy.
  7. Forward bytes unchanged and set download headers on your server.
  8. Keep API keys on the server, not in browser JavaScript.

Frequently Asked Questions

Can I use FileReader to fix an empty PDF?

FileReader only reads data you already received; it cannot recover bytes hidden by CORS or replace an incorrect Fetch or Axios response type.

Should I trust Content-Length as proof that a PDF is valid?

No. It confirms a reported byte count, not document structure. Validate the content type and, when diagnosing, the PDF signature as well.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Why does opening the API URL in a browser work when fetch fails?

Top-level navigation is not the same as script access. The browser may display a cross-origin response while still blocking JavaScript from reading it because of CORS.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.