Browsers decide whether to display a response, download it, or hand it to an
external application from several inputs. Content-Disposition provides a
strong preference, but content type, navigation context, sandboxing, browser
capabilities, and user settings also affect the result.
Inline and Attachment Dispositions #
An attachment response requests a download:
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename="guide.pdf"
Content-Length: 110352
X-Content-Type-Options: nosniffAn inline response permits presentation inside the browser:
Content-Type: application/pdf
Content-Disposition: inline; filename="guide.pdf"inline does not guarantee display. The browser needs a viewer for the media
type, and the user can configure some types to download instead. Omitting the
header leaves the choice primarily to the media type and navigation context.
Supplying a Safe Filename #
filename carries an ASCII-compatible fallback. filename* uses the extended
parameter encoding defined by RFC 8187, which supersedes RFC 5987, and can
represent Unicode:
Content-Disposition: attachment;
filename="report.pdf";
filename*=UTF-8''r%C3%A9sum%C3%A9.pdfSend both parameters because older clients may ignore filename*; conforming
recipients prefer filename* when both are valid. Treat a server-supplied
filename as untrusted input:
- Remove directory separators and control characters.
- Reject
.and..as final names. - Keep the expected extension consistent with the media type.
- Impose a reasonable length limit.
- Never construct a server filesystem path directly from the value.
Browsers sanitize filenames independently, so exact renaming behavior differs between implementations.
Content Type and Sniffing #
Content-Type describes the representation. A PDF should be
application/pdf, an SVG should be image/svg+xml, and arbitrary binary data
should be application/octet-stream.
X-Content-Type-Options: nosniffnosniff makes script and style fetches reject incompatible media types and
limits MIME-based reinterpretation in other protected fetch paths. It does not
repair an incorrect Content-Type, make arbitrary content safe, or force a
download. Serving active content under an incorrect benign type remains a
security problem because navigation and embedding rules depend on both the
declared type and the request context.
HTML Download Links #
The download attribute expresses download intent for a link:
<a href="/exports/report.csv" download="report.csv">Download report</a>For HTTP(S) URLs, browsers honor download only for same-origin resources.
blob: and data: URLs are also eligible; CORS response headers do not make a
cross-origin HTTP URL eligible. A response Content-Disposition filename can
affect the suggested name, and attachment remains the reliable server-side
mechanism for a cross-origin download. Authorization must remain on the
download endpoint because hiding a URL behind a button does not protect it.
Blob URLs support generated client-side files:
const blob = new Blob([csv], { type: "text/csv;charset=utf-8" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "report.csv";
document.body.append(link);
link.click();
link.remove();
setTimeout(() => URL.revokeObjectURL(url), 0);Revoke object URLs after the browser has started the navigation because each URL retains its backing data. Run this sequence from a user-initiated event; programmatic clicks do not bypass browser download policy.
Automation and Security Constraints #
Sandboxed frames need the allow-downloads sandbox token, and browsers can
still block downloads without transient user activation or throttle repeated
downloads.
Cross-origin isolation, popup policy, mixed-content policy, and enterprise
configuration can introduce additional restrictions.
Test the complete navigation in each supported browser:
- Verify status,
Content-Type, andContent-Disposition. - Confirm the suggested filename and downloaded bytes.
- Exercise authenticated, redirected, and error responses.
- Verify that HTML error pages are not saved under a trusted file extension.
Header inspection alone cannot prove the user-visible outcome because the browser performs the final policy decision.
Comments
Read or comment on GitHub (sign-in required to comment)