2025, Nov 04 13:00

Blank PDF in the Browser? How to Return and Download Binary Data as a Blob (fpdf2, Django, Vue/TS)

Fix a blank PDF in the browser with fpdf2, Django, and a TypeScript/Vue client: request the response as a Blob to keep binary bytes intact and render correctly.

When a PDF renders as a blank page in the browser but looks fine when saved on the server, the issue is often not the generator. It’s the way the bytes travel to and are interpreted by the client. Here’s a concise walkthrough of a real case with fpdf2, Django, and a TypeScript/Vue front end, where the server produced a valid file yet the browser displayed an empty document. The fix was purely on the client side: request the response as a Blob instead of a default text/JSON path.

Minimal setup that reproduces the blank PDF

The backend builds a very simple PDF with fpdf2 and returns it in an HTTP response. The file looks correct if dumped to disk from Python. The problem appears only after hitting the route from the front end.

# file: taskGeneratePDF.py
from fpdf import FPDF
from api.packages.utils.jsonLogger import LogFormat, get_or_create_json_logger
activity_log = get_or_create_json_logger(__name__, log_format=LogFormat.DEFAULT)
def build_task_pdf(job_payload: dict, out_path: str) -> bytearray:
    pdf_doc = FPDF()
    pdf_doc.add_page()
    pdf_doc.set_font("helvetica", style="B", size=16)
    pdf_doc.cell(40, 10, "Hello World!")
    activity_log.info(f"PDF generated and saved to {out_path}")
    file_bytes = pdf_doc.output()
    return file_bytes

The route returns the generated bytes with the expected PDF headers.

# file: TaskRoutesId.py
class TaskPdfRoute(GuardedRouteBase):
    def get(
            self,
            request: HttpRequest,
            *args,
    ) -> HttpResponse:
        try:
            pdf_bytes = build_task_pdf({"test": "test"}, "test.pdf")
            hdrs = {"Content-Disposition": "attachment; filename=myfilename.pdf"}
            return HttpResponse(bytes(pdf_bytes), content_type="application/pdf", headers=hdrs)
        except Exception as exc:
            return HttpResponse({"error": f"Failed to generate PDF: {str(exc)}"}, status=404)

The client invokes the endpoint and attempts to download the response as a PDF.

// file: TaskClient.ts
export async function fetchTaskPdf(taskId: string, payload: TaskPDFPayload) {
  try {
    const response = await httpGet<Uint8Array>(`/task/${taskId}/pdf/`)
    return response
  } catch (err) {
    return await Promise.reject(err)
  }
}
// file: TaskMainDetails.vue
function trigger_pdf_download() {
  const payload = {}
  const jobId = props.task?.vertexID || ""
  const pdfPromise = TaskClient.fetchTaskPdf(jobId, payload)
  pdfPromise.then((msg) => {
    const blob = new Blob([msg.data], { type: "application/pdf" })
    const a = document.createElement("a")
    a.href = window.URL.createObjectURL(blob)
    a.download = "test_pdf_name"
    a.click()
  })
}

What’s actually happening

The PDF bytes arrive, but the client handles them through a request helper that is not configured for binary payloads. When the response is treated as text or a default format, the byte sequence is altered and the viewer receives a file that technically opens but renders blank. Verifying the body as text shows recognizable PDF structure, which further points to a transport/decoding mismatch rather than a generator failure.

The one-line fix

Request the response as a Blob on the client. That ensures the binary data remains intact end-to-end.

I needed to use httpGetBlob<Blob> in TaskClient.ts instead of the httpGet

Here’s the adjusted client code.

// file: TaskClient.ts
export async function fetchTaskPdfBlob(taskId: string, payload: TaskPDFPayload) {
  try {
    const response = await httpGetBlob<Blob>(`/task/${taskId}/pdf/`)
    return response
  } catch (err) {
    return await Promise.reject(err)
  }
}
// file: TaskMainDetails.vue
async function trigger_pdf_download() {
  const payload = {}
  const jobId = props.task?.vertexID || ""
  const res = await TaskClient.fetchTaskPdfBlob(jobId, payload)
  const blob = new Blob([res.data], { type: "application/pdf" })
  const a = document.createElement("a")
  a.href = window.URL.createObjectURL(blob)
  a.download = "test_pdf_name"
  a.click()
}

The backend code can stay as is, returning bytes with content_type set to application/pdf and a Content-Disposition header for download. The critical change is the client using a Blob-aware request helper.

Why this matters

PDFs, images, archives, and other binary assets must remain byte-accurate between server and browser. If a client library or helper treats a binary stream as text or JSON, subtle alterations break the file even though the response technically “succeeds.” By explicitly requesting a Blob, you preserve the original bytes and avoid blank renderings, corrupted downloads, or viewers that refuse to open the document.

Takeaways

If your server-side generator produces a valid PDF, but the browser shows an empty page while the same data looks fine as plain text, check the client transport first. Use a Blob-specific request path for binary responses. Keep the server response type as application/pdf and return the raw bytes. With those pieces aligned, the PDF downloads and displays as intended.

The article is based on a question from StackOverflow by Joe and an answer by Joe.