Treat the download as a complete transaction

A customer taps Download for an invoice, booking confirmation or service record. The business may have checked that the customer can see the page, yet a separate file endpoint can still return the wrong bytes, an unsafe filename or a response that the browser interprets unexpectedly. A usable design begins with one server-side file identity, a current entitlement check and a response contract. Decide what the customer is allowed to obtain, which format is promised, how the saved file will be named, and what happens if the file is unavailable. A clickable link by itself does not prove any of these outcomes.

Keep the entitlement check at the file request, not only on the page that links to it. A link can be copied, revisited after sign-out or opened from a different account. The server should resolve the opaque file identifier to a record, confirm the current requester may access that record, and then stream the approved bytes. Avoid exposing a storage path or relying on a predictable filename as the permission rule. OWASP's file guidance recommends an application handler mapped from an identifier to a stored file, especially when file retrieval must be controlled. An expired or unauthorised request needs a deliberate error response without leaking the document.

Declare the actual type

The Content-Type header should describe the file actually returned. A PDF should not be labelled as HTML, and an unknown binary should not be guessed from an untrusted upload name. MDN explains that a media type tells the browser what kind of content it has received; browsers may otherwise try to infer a type from the bytes. Set X-Content-Type-Options: nosniff so the declared type is respected, while also making sure the declared type is correct. Nosniff cannot repair a wrong type or make a malicious file safe to open. If the business accepts customer-provided files, apply input validation and processing before making any file available for download.

Think through the difference between a generated invoice and a customer-uploaded attachment. The invoice generator should know its output format and can set an exact PDF media type when it truly emits PDF bytes. An attachment needs a documented allowlist and verification process; the client's declared upload type is not reliable proof. OWASP warns that file type declarations can be spoofed and recommends validating the content, limiting allowed formats and storing files away from directly executable web paths. Those are upstream controls. The download response should not undo them by serving an untrusted format under a benign label or by making it executable at the shop's origin.

Choose download or inline display deliberately

Content-Disposition controls whether the browser should display a response inline or handle it as an attachment. For a document the customer is expected to save, attachment is a clear default; include a safe filename chosen by the server. MDN notes that the filename parameter can influence the suggested saved name and that filename* supports an encoded non-ASCII name when provided alongside an ASCII fallback. The server should construct both from trusted record data, remove path separators and control characters, and keep an extension consistent with the verified type. Do not echo an untrusted upload filename directly into an HTTP header.

Inline viewing may be a valid product decision for a verified, safe PDF preview, but it changes the customer's expectations and browser exposure. Ask whether the user must be able to save a copy, whether the page has an accessible alternative, and whether opening in a new tab leaves the private file visible on a shared device. An HTML download attribute on a link is not a substitute for the server response: MDN documents interactions between that attribute and Content-Disposition, including the response filename taking precedence for the suggested name. Test the actual result in supported browsers rather than relying on one attribute in the template.

Match caching to the privacy promise

A private invoice or service record should not be placed in a shared cache that could serve it to another person. MDN defines Cache-Control: private as allowing a browser's private cache but excluding shared caches; no-store asks caches not to store the response at all. Choose between them based on the document's sensitivity and the business's device-sharing assumptions. A highly sensitive one-time export may warrant no-store, while another authenticated document may use a different policy after risk review. Apply the rule to the file response itself, including error and redirect paths where personal data might appear, rather than assuming the HTML page's policy carries over.

A browser save operation creates a local file outside the shop's control. No HTTP cache directive can retract that saved copy. Tell customers in plain language when a document contains personal information and avoid promising remote deletion of copies already downloaded. If the business uses time-limited signed URLs, review their lifetime, scope and leakage paths, but still ensure the resulting file response has the intended type, disposition and cache policy. A signed URL is a way to grant temporary access; it is not automatically a complete download contract or a substitute for checking whether the right person should receive the link.

Work through a customer example

Imagine a customer opening the order area to download a PDF receipt. The visible link refers to an opaque document identifier, not a storage filename. On request, the service checks the signed-in account against that order, retrieves the approved PDF bytes and returns a PDF Content-Type, nosniff, an attachment disposition with a safe receipt filename, and the chosen private cache policy. The customer receives a file whose name and contents match the order shown on screen. These are design choices in a worked example, not a claim that any existing shop currently implements them.

Now change one condition at a time. If the customer signs out, the old link should produce an unauthorised or sign-in result without returning PDF bytes. If another account opens the copied link, the service should not reveal the first customer's receipt. If the document generator has failed, explain that the file is unavailable and offer a later retry or support route; do not return an HTML error page named receipt.pdf. If the stored bytes do not match the expected format, stop delivery and investigate instead of attaching a misleading PDF label. Each case gives a concrete acceptance result for both engineering and support staff.

Inspect the actual HTTP exchange

Test from the customer interface and inspect the network response, not just the filename in a browser's download shelf. Record status, final URL after redirects, Content-Type, X-Content-Type-Options, Content-Disposition, Cache-Control and the first bytes or a verified file signature in a safe test environment. Confirm that the file opens in the expected reader and matches the right record. Repeat on a fresh session, an expired session and a different customer's session. Include a filename with Chinese characters and spaces to see how the supported browsers display the ASCII fallback and encoded name.

Check error handling with the same care as a successful download. A network failure should leave the order page understandable and the customer able to retry without guessing whether a file was saved. A denied request should not disclose another customer's name in the error body or suggested filename. A partial or corrupt file should not be presented as a completed receipt. Support staff need a reference to the order and download attempt, but logs should avoid storing the document itself or secret access tokens. The release is ready when the intended customer obtains the correct file and all tested unauthorised routes return no private bytes.

Make the promise clear before the tap

The download control should name the document and its format in ordinary language, such as “Download receipt (PDF),” rather than only “Download.” If the file is generated on demand, tell the customer when it is preparing and when it is ready. If the document covers a particular order or period, show that context outside the file so the person can choose the right one. Do not display a confident ready state until the server can actually supply the approved bytes. This front-end promise should match the response contract: format, suggested name and access result.

Ask a support colleague to perform the journey without developer tools, then let an engineer inspect the same request at the network level. The colleague should identify the document, find the saved file and understand an expired-session message. The engineer should verify the response headers and that a denied path returns no document bytes. Both views are needed: a technically correct attachment can still be hard to find, while a pleasant download button can conceal a wrong or exposed file. Reconcile the two observations before the release is accepted.

Before you proceed

  • Is entitlement checked at the file request?
  • Do type, disposition and filename describe the actual bytes?
  • Do private cache and failure paths match the document sensitivity?

Sources & further reading