PDFPipe

The request or the response

The PDF opens in the browser instead of downloading

Clicking the link shows the document in the browser's built-in viewer. You wanted a file in the downloads folder with a sensible name.

What is actually happening

Content-Disposition is absent or set to inline. Browsers display PDFs they can render unless told to treat the response as an attachment, and the filename comes from the same header rather than from the URL.

Confirming it is this and not something that looks like it

Look at the response headers. If Content-Disposition is missing, that is the whole answer.

The fix

Set Content-Disposition to attachment with a filename. Quote the filename and encode it if it can contain non-ASCII characters, which for anything containing a customer name it can.

javascript
import { contentDisposition } from "./http.js";

// Handles quoting and the RFC 5987 encoding for non-ASCII names.
res.setHeader("Content-Type", "application/pdf");
res.setHeader(
  "Content-Disposition",
  `attachment; filename="invoice-${id}.pdf"; filename*=UTF-8''${encodeURIComponent(
    `facture-${customer.name}-${id}.pdf`,
  )}`,
);

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.

  • inline, which is the deliberate opposite and sometimes what you actually want
  • A filename with a comma or a quote, which breaks the header without an error
  • Non-ASCII filenames, which need the filename* form to survive
  • A download attribute on the anchor, which only works same-origin

Frequently asked

Does this happen with every rendering engine?

The behaviour behind it is not specific to one tool. Content-Disposition is absent or set to inline. 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.

Paste your markup and see the rendered document, without signing up.