Response header
< Content-Disposition: attachment; filename="report.pdf"Content-Disposition Header
Learn how the Content-Disposition header controls whether content displays inline or downloads as an attachment. Set custom filenames for file downloads.
- Direction
- Response
- Category
- Representation
- Spec
- RFC 6266
On this page
TL;DR:
Content-Disposition: attachmenttells the browser to download the response instead of displaying it;inline(the default behavior) displays it. Addfilename="report.pdf"to suggest an ASCII save name, andfilename*=UTF-8''...for a name with non-ASCII characters. The filename is only a suggestion, and receivers treat it as untrusted input.
What is Content-Disposition?
On a response, inline asks the browser to handle the content normally for its media type, and attachment asks it to save the file. The browser still has the final say: user settings and its own handling decide whether you get a dialog or a direct save. RFC 6266 §4.2 defines both disposition types.
How Content-Disposition Works
A download response looks like this (example headers, PDF body left out):
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename="report.pdf"
Content-Type still says what the file is. The disposition only changes how the browser handles it, so marking HTML as an attachment downloads HTML.
Syntax
Content-Disposition: inline
Content-Disposition: attachment; filename="report.pdf"
Content-Disposition: attachment; filename="report.pdf"; filename*=UTF-8''rapport-%C3%A9t%C3%A9.pdf
Those are two single apostrophes after UTF-8, with an empty language tag between them, not a double quote. Each parameter can appear once; repeating a name makes the whole field invalid.
Common Examples
Quote an ASCII name that contains spaces:
Content-Disposition: attachment; filename="monthly report.csv"
For international names, send an ASCII fallback in filename and the real name in filename*. Browsers that understand both use filename*.
Real-World Scenarios
For a CSV export, send Content-Type: text/csv and a disposition that names the file. On a same-origin link, the HTML download attribute can override inline in Chrome and Firefox; MDN’s handling notes have the details.
A preview endpoint and a download endpoint can serve identical PDF bytes. Send inline for the preview and attachment for the download, and keep application/pdf on both. The only difference is what you’re asking the browser to do with it.
A plain link is all you need for a download:
<a href="/report.pdf">Download the report</a>
The response headers decide the disposition. If you reach for the <a download> attribute instead, it only works for same-origin URLs and blob: or data: URLs, per MDN’s anchor reference.
Server Implementation
In nginx, add this to a server block. The route returns a tiny CSV:
location = /export.csv {
default_type text/csv;
add_header Content-Disposition 'attachment; filename="export.csv"';
return 200 "status\nready\n";
}
add_header adds the disposition and default_type sets the media type. Let default_type own Content-Type; adding another one with add_header gives you two.
In Express, let the download helpers build the filename parameter for you:
app.get('/report.pdf', (req, res, next) => {
res.download('/srv/reports/report.pdf', 'report.pdf', err => {
if (err) next(err)
})
})
app.get('/export.csv', (req, res) => {
res.attachment('export.csv').send('status\nready\n')
})
res.download() sends a file from disk. res.attachment() only sets the disposition and guesses a media type from the extension; you send the body yourself. Both paths here are fixed server paths, so a request parameter can’t steer them anywhere. If you build a path from user input, validate it first. A download can fail after the headers are already out, which is why the callback passes the error to the app’s error handler instead of trying to start a second response.
In Django, use FileResponse:
from django.http import FileResponse
def download_report(request):
return FileResponse(
open('/srv/reports/report.pdf', 'rb'),
as_attachment=True,
filename='report.pdf',
)
Django closes the file for you once the response is sent. A with block would close it too early, before the response body is read. Both examples assume the file exists and that you’ve checked authorization before opening it.
With Apache and mod_headers loaded, a file-scoped rule marks CSV exports as attachments:
<Files "export.csv">
Header set Content-Disposition "attachment; filename=export.csv"
</Files>
The filename can go unquoted here because it has no spaces or separator characters. If other directories have files with the same name, scope the rule to the export directory. The rule only sets the disposition; Apache’s normal file handler still serves the body and media type.
Getting Content-Disposition right
Treat filenames as advisory. Whenever your code receives one, from a download or an upload, strip path components, reject control characters and unsafe names, and decide whether a file is executable by something sturdier than its extension. RFC 6266 §4.3 lists these receiver-side precautions. When sending, keep raw user input out of response headers.
A value like filename="../../report.pdf" should never let a receiver write outside its download directory, and a suggested name should never decide whether an upload is safe to process. When sending, look the file up from an authorized record and pick the save name separately. The path on disk and the name the user sees do different jobs.
Filename Encoding
filename* uses the extended parameter syntax, now specified in RFC 8187: percent-encode the UTF-8 bytes. Keep percent escapes out of the plain filename, since browsers disagree about whether to decode them.
For a known name your application controls, you can build the extended value in JavaScript:
const filename = 'rapport-été.pdf'
const encoded = encodeURIComponent(filename).replace(
/['()*]/g,
character => `%${character.charCodeAt(0).toString(16).toUpperCase()}`
)
res.set('Content-Disposition',
`attachment; filename="rapport-ete.pdf"; filename*=UTF-8''${encoded}`)
encodeURIComponent() leaves a few characters unescaped that this parameter syntax doesn’t allow literally, and the extra replace escapes them. Pick the ASCII fallback yourself. Encoding makes a name transmittable; it doesn’t make it a safe name.
Form Data Usage
Inside multipart/form-data, the same header describes one part of the body, not the whole response. Part headers look like this:
Content-Disposition: form-data; name="upload"; filename="report.pdf"
Content-Type: application/pdf
name identifies the form field. RFC 7578 forbids filename* in form-data parts, so download syntax and upload-part syntax aren’t interchangeable.
Let curl build the multipart body and boundary for you:
curl -v -F 'upload=@report.pdf;type=application/pdf' https://example.com/upload
-F assembles the multipart body and the Content-Type boundary. Whatever receives the upload still has to validate it, because the sender supplies both the part’s media type and its filename.
Testing Content-Disposition
Check a GET response’s headers without saving the body:
curl -sS -D - -o /dev/null https://example.com/export.csv
Then open the download URL in a browser. fetch() just retrieves a response; it never opens a save dialog by itself. If cross-origin JavaScript needs to read this header, expose it with Access-Control-Expose-Headers: Content-Disposition alongside the rest of your CORS policy, as the Fetch Standard defines.
When the app needs to check a response before offering it as a download, validate the status and then create a blob URL:
async function downloadReport() {
const response = await fetch('/report.pdf')
if (!response.ok) throw new Error(`HTTP ${response.status}`)
const url = URL.createObjectURL(await response.blob())
const link = document.createElement('a')
link.href = url
link.download = 'report.pdf'
document.body.append(link)
link.click()
link.remove()
return url
}
This uses a fixed filename rather than parsing Content-Disposition. Once the download UI is done with the URL, call URL.revokeObjectURL(url) to free it. Keep in mind that response.blob() reads the entire body into memory first, so for big files a direct link that the browser streams to disk is the better choice.
Related Headers
Frequently asked questions
What is Content-Disposition?
Content-Disposition requests inline media-type handling or saving as an attachment. It can suggest a filename, but the browser controls the download UI.
How do I force a file download?
Use Content-Disposition: attachment; filename="file.pdf" to request saving the response. filename suggests a save name; the browser controls whether it shows a dialog or saves directly.
How do I handle special characters in filenames?
Use filename* with UTF-8 percent-encoded bytes and two apostrophe delimiters after UTF-8. Include an ASCII filename fallback alongside it.
What is the difference between inline and attachment?
inline requests normal media-type handling; attachment requests saving the response. The browser decides the download UI, and the suggested filename is advisory.
Sources
Related
Content-Language Header
Learn how the Content-Language header specifies the natural language(s) of response content. Understand language tags and internationalization best practices.
Content-Length
Learn how Content-Length specifies the body size in bytes. Essential for progress indicators, connection management, and chunked transfer decisions.
Content-Location Header
Learn how Content-Location indicates an alternate URL for returned content. Useful for content negotiation and identifying canonical resource locations.
Date Header
Learn how the Date header specifies when the HTTP message was originated by the server. Understand date formats and its role in caching and logging.