The request or the response
The downloaded PDF is corrupted and will not open
The file downloads at roughly the right size and the viewer refuses it. Opening it in a text editor shows %PDF at the start, so the bytes arrived, and something has changed them.
What is actually happening
The bytes were treated as text somewhere. A response read as a UTF-8 string and written back out corrupts every byte sequence that is not valid UTF-8, which in a PDF is most of them. The file size stays plausible, which is why this is diagnosed late.
Confirming it is this and not something that looks like it
Compare the byte length of what you received against the Content-Length header. A mismatch, particularly a larger number, means an encoding step replaced bytes with multi-byte replacement characters.
The fix
Keep the response as bytes from end to end. Every language has a distinct binary path and a distinct text path, and this bug is always the text path being taken by accident.
# Wrong: .text decodes to str, and the decode is lossy for binary content.
pdf = response.text.encode()
# Right: .content is bytes, and stays bytes.
pdf = response.content
# Right, and better for large documents: never fully resident.
with client.stream("POST", url, json=payload) as response:
with open(path, "wb") as fh: # "wb", not "w"
for chunk in response.iter_bytes():
fh.write(chunk)If that was not it
These produce the same symptom often enough to be worth ruling out before assuming the fix above did not work.
- Opening the output file in text mode, which on Windows also translates line endings
- A base64 round trip with padding lost or newlines inserted
- A proxy or middleware that compresses or rewrites response bodies
- Ruby, where Net::HTTP returns ASCII-8BIT and anything assuming UTF-8 will corrupt it
Frequently asked
Does this happen with every rendering engine?
The behaviour behind it is not specific to one tool. The bytes were treated as text somewhere. Anything rendering HTML to a paged medium has to make the same decision, so the fix travels with you if you change how the render happens.
Will this show up as an error in my logs?
Yes, this one surfaces as a thrown error or a failed status, which is why it is at least findable. The harder half is that the message often names the call that was in flight rather than the thing that actually failed.
Related failures
Problems people arrive at from the same starting point, or mistake for this one.
The PDF opens in the browser instead of downloading
Content-Disposition is absent or set to inline.
The PDF is truncated or ends mid-document
The response was cut off.
413 Payload Too Large when sending HTML to render
The request body exceeded a limit.
401 Unauthorized from the render API
The key that arrived is not the key you think you sent.
429 Too Many Requests when generating PDFs in bulk
Unbounded concurrency.
CSS for paged documents, and what actually works
Which parts of the paged media specification a browser-based render implements.
Everything that goes wrong, by category
The full list, grouped by where in the pipeline it breaks.
Paste your markup and see the rendered document, without signing up.