Free tools Windows power users keep installed
One-click scans. No signup required.
An empty or null PDF URL usually means a value failed somewhere in the download pipeline—not that the PDF format itself is broken. Check the endpoint value, HTTP response, CORS visibility, response body, Blob conversion, and final link assignment in that order. A reliable browser flow is: verify response.ok, read the bytes with response.blob(), confirm the Blob is nonempty, then call URL.createObjectURL(blob) and assign the returned string to the link.
What an empty PDF URL actually means
There are several different failures that look identical in a user interface:
- The application never received a PDF endpoint, so the original variable is
nullor an empty string. fetch()reached a server that returned 404, 401, 403, or 500. Fetch normally fulfills with aResponsefor these HTTP statuses, so code that skips a status check may continue with an error page.- The response is opaque. Its status is 0, headers are unavailable, and its body is inaccessible to JavaScript. Calling
blob()on an opaque response produces a zero-size Blob with an empty type, which cannot make a useful download. - The response body was never converted into a Blob URL, or an asynchronous assignment happened after the click.
- The link received a nullable intermediate value instead of the string returned by
URL.createObjectURL().
Find the first point at which the value becomes missing. Logging only the final anchor hides the real cause.
Trace the value from endpoint to anchor
1. Log the original endpoint
Before calling fetch(), log the exact value supplied by route parameters, application state, or your API response:
#1 Best Overall
console.log("PDF endpoint before fetch:", endpoint);
If it is null, undefined, or "", inspect the code that builds that value. Common causes include a missing record ID, a property-name mismatch, a route that has not finished loading, or an API response whose PDF field is nullable.
2. Inspect the Network panel
Open browser developer tools, select the Network tab, reproduce the download, and inspect the request URL, status, redirects, response headers, and response preview. A successful network transaction can still return a login page, JSON error, or proxy-generated HTML instead of PDF bytes.
3. Log the value immediately before assignment
console.log("href before click:", link.href);
If the endpoint was populated but href is empty, check that the asynchronous fetch and Blob conversion have completed before the user clicks. Also inspect every error branch; a catch block that silently assigns null can conceal the original exception.
Use a safe fetch-to-Blob URL flow
This pattern validates status, media type, and body size before creating a temporary browser URL:
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
async function preparePdfDownload(endpoint, link) {
if (!endpoint) throw new Error("PDF endpoint is missing");
const response = await fetch(endpoint);
if (!response.ok) {
throw new Error(`PDF request failed: ${response.status} ${response.statusText}`);
}
const contentType = response.headers.get("content-type");
if (!contentType || !contentType.toLowerCase().includes("application/pdf")) {
throw new TypeError(`Expected application/pdf, got ${contentType ?? "no content type"}`);
}
const blob = await response.blob();
if (blob.size === 0) {
throw new Error("PDF response body is empty");
}
const objectUrl = URL.createObjectURL(blob);
link.href = objectUrl;
link.download = "document.pdf";
link.dataset.objectUrl = objectUrl;
}
const link = document.querySelector("#download-pdf");
preparePdfDownload("/api/invoices/123.pdf", link)
.catch(error => {
console.error(error);
link.removeAttribute("href");
});
response.blob() consumes the response body and returns a Blob. URL.createObjectURL() then returns a blob: URL string; the Blob’s bytes themselves are not a URL. The content-type and size checks are practical safeguards: a content type alone cannot prove that the bytes are a valid PDF, but it can reveal an HTML or JSON response early.
Release object URLs at the right time
Object URLs hold browser resources. Revoke them after the download has had time to start, not before the browser consumes the URL:
link.addEventListener("click", () => {
const objectUrl = link.dataset.objectUrl;
if (objectUrl) {
setTimeout(() => URL.revokeObjectURL(objectUrl), 1000);
}
}, { once: true });
Revoking immediately after assigning href can make the link unusable. For repeated downloads, revoke the previous URL before replacing it, once no earlier click needs it.
Choose the right download architecture
| Approach | Use when | Checks and trade-offs |
|---|---|---|
| Direct server URL | Your backend returns a stable, authorized download address. | Inspect the final redirected URL, origin, status, Content-Disposition, and media type. The browser can stream the file without loading all bytes into JavaScript. |
| Fetched bytes plus Blob URL | The client must use credentials, inspect, transform, or gate the PDF before download. | Requires readable CORS headers, status validation, a nonempty body, and memory for the Blob. The resulting blob: URL is temporary. |
When a direct URL is preferable
If the server can authenticate the request and set a useful filename, return that URL directly. Response.url reports the final URL after redirects, which helps detect an unexpected login or storage endpoint:
const response = await fetch(downloadUrl, { redirect: "follow" });
console.log("Final response URL:", response.url);
if (!response.ok) throw new Error(`Download failed: ${response.status}`);
For a server-served file, Content-Disposition can suggest a filename and whether the response is treated as an attachment. Browser handling varies, especially when a link’s download attribute conflicts with server headers.
When a Blob URL is preferable
Use the Blob route when the PDF endpoint requires cookies or an authorization header and you do not want to expose those credentials in a public link. Reading the entire response means larger files consume more client memory, so a direct server download is generally better for very large documents.
CORS and opaque responses
Do not use mode: "no-cors" as a workaround when your code needs PDF bytes. That mode can produce an opaque response: JavaScript cannot read its headers or body, and response.blob() yields a zero-size, empty-type Blob. Configure the PDF server to allow the requesting origin with normal CORS headers, then use the default fetch mode or an explicitly configured CORS request.
Credentials add another constraint: if cookies are required, use the appropriate credentials setting and configure the server’s allowed origin and credential headers together. A permissive wildcard origin cannot be combined with credentialed requests.
Make the anchor behave like a download
Assign a nonempty href before the click. The download attribute applies to same-origin URLs and to blob: or data: URLs; it is not a universal command to save cross-origin content. The browser may open, save, or prompt according to its settings and the response headers.
<a id="download-pdf" download="document.pdf">Download PDF</a>
If the link opens an HTML login page, fix authentication or the endpoint rather than changing the filename. If the browser ignores the suggested name, inspect Content-Disposition and test the final response URL.
Common symptoms, causes, and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
endpoint is null |
State, route parameter, or API field is missing. | Log the source value and render or fetch only after required data exists. |
| Fetch resolves with 404/500 | HTTP errors do not automatically reject fetch. | Check response.ok or response.status and show the actual error. |
| Status 0, empty headers, zero Blob | Opaque response, often caused by no-cors or missing CORS configuration. | Configure server CORS and remove mode: "no-cors". |
| Blob URL exists but file is invalid | Body is HTML, JSON, or a proxy error. | Inspect content type, preview bytes, authentication, and the response body. |
| Click does nothing | href was assigned after the click, is empty, or the object URL was revoked too early. |
Await preparation, log link.href, and revoke only after use. |
| Filename or inline behavior is unexpected | Content-Disposition, media type, origin, or browser policy. |
Inspect response headers and test the target browser’s behavior. |
Or skip the browser setup
If your goal is to obtain a PDF or image capture from a web page rather than debug an application’s own PDF endpoint, ScreenshotNeo provides a single API request. It can capture a page as PDF, accepts cookies and consent banners before capture, removes more than 60 known consent platforms, newsletter popups, and chat widgets, and lets you turn those steps off. Failed loads, blank pages, bot checks, CAPTCHAs, timeouts, and cache hits are not billed; response headers identify the page verdict and billing result.
Example cURL request (see the ScreenshotNeo API documentation for options):
Recommended Free Tools
Rank #4
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
For a PDF, request the PDF output option documented by the API. The same service also offers an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. Free accounts include 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
Reliability, performance, and security notes
- Validate the final URL and response rather than trusting a URL returned by an untrusted client.
- Do not log access tokens, signed URLs, or private PDF contents in production browser logs.
- Abort requests that exceed your application’s acceptable wait time, and provide a retry path for transient failures.
- For large files, prefer a server download or streaming architecture when possible; Blob conversion keeps the complete body available to the browser.
- Cache only when authorization and document freshness permit it. A stale cached URL can look valid while pointing to an expired resource.
FAQ
Does a null URL mean the PDF is empty?
No. A null URL usually means your application failed before it created a link. Check the endpoint variable and response pipeline separately from the PDF bytes.
Can I create a URL directly from a Blob?
Use URL.createObjectURL(blob); the Blob object itself is not a string URL.
Why does fetch not enter catch for a 404?
HTTP error statuses normally produce a fulfilled Response. Reject explicitly after checking response.ok.
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Should I revoke a Blob URL immediately?
No. Revoke it after the browser has started consuming the download, otherwise the link may fail.
Quick Recap
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.




