cfcontent and cfheader together are how a CFML page serves a file directly to the browser instead of rendering a normal page, cfheader sets the response header that decides how the browser treats it, cfcontent actually streams the file's bytes.
Learning Objectives
After completing this lesson, you'll be able to:
- Serve a file for download with cfcontent.
- Force a "Save As" dialog with cfheader's Content-Disposition header.
- Choose between inline and attachment delivery deliberately.
- Avoid building a file path directly from untrusted input.
How a File Download Actually Happens
Request for /download.cfm
cfheader Sets Content-Disposition
cfcontent Streams the File
Browser Prompts to Save
A Basic Forced Download
<cfheader name="Content-Disposition" value="attachment; filename=report.pdf"> <cfcontent type="application/pdf" file="/var/data/reports/report-2026.pdf">
cfheader(name = "Content-Disposition", value = "attachment; filename=report.pdf"); cfcontent(type = "application/pdf", file = "/var/data/reports/report-2026.pdf");
Content-Disposition: attachment is what forces a "Save As" dialog instead of the browser trying to render the file inline.
cfcontent's Attributes
| Attribute | Meaning |
|---|---|
| type | The MIME type of the response (e.g. application/pdf, application/vnd.ms-excel) |
| file | A server-side file path whose contents become the page output |
| variable | A binary variable (like a generated PDF or chart) to output instead of a file |
| deleteFile | Deletes the file from disk after it's sent (default no) |
| reset | Discards output rendered so far (default yes); has no effect when file is set |
Once file or variable is specified, cfcontent halts further page processing, similar to cfabort, nothing after it on the page runs.
cfheader's Attributes
| Attribute | Meaning |
|---|---|
| name | The header name to send (required unless statusCode is used) |
| value | The header's value |
| statusCode / statusText | Sets an HTTP status code instead of (or alongside) a named header |
| charset | Character encoding for the header value, defaults to UTF-8 |
Since ColdFusion MX 6.1, cfheader name="Content-Disposition" encodes the header's value using the file's own character encoding, so a filename containing non-ASCII characters is handled correctly.
A Real Example: Inline vs Attachment, Deliberately
<cfheader name="Content-Disposition" value="inline; filename=invoice.pdf"> <cfcontent type="application/pdf" file="#invoicePath#">
<cfheader name="Content-Disposition" value="attachment; filename=invoice.pdf"> <cfcontent type="application/pdf" file="#invoicePath#">
inline lets a browser that can render the MIME type (most browsers, for a PDF) display it directly, attachment always prompts to save. The choice is entirely the Content-Disposition value, cfcontent itself behaves the same either way.
A Real Mistake to Avoid: Building the File Path From Untrusted Input
<cfparam name="url.fileId" type="numeric">
<cfquery name="fileRecord" datasource="myDsn">
SELECT filePath, fileName FROM downloads WHERE id = <cfqueryparam value="#url.fileId#" cfsqltype="cf_sql_integer">
</cfquery>
<cfif fileRecord.recordCount EQ 0>
<cfthrow type="application" message="File not found.">
</cfif>
<cfheader name="Content-Disposition" value="attachment; filename=#fileRecord.fileName#">
<cfcontent type="application/octet-stream" file="#fileRecord.filePath#">The actual file path never comes from the request directly, only an id does, which is looked up against a known, server-controlled path. Building a path like "/downloads/" & url.fileName directly from user input risks a path-traversal request (something like ../../etc/passwd) reaching cfcontent's file attribute.
A Real Lucee-Specific Attribute: range
Lucee's cfcontent documents a range attribute controlling whether byte-range requests (used for resumable or partial downloads, like a video player seeking) are permitted, left to the client's discretion by default. This attribute isn't documented on Adobe ColdFusion's cfcontent.
Common Beginner Mistakes
Building a file path directly from a URL or form parameter
This risks a path-traversal attack. Validate a requested identifier against a database or an allowlist, and only ever build the actual file path from that trusted, server-side value.
Calling cfheader after cfflush
This throws an error, cfheader must run before any output has actually been flushed to the client.
Setting reset while also setting file
reset has no effect once file is specified, they're effectively mutually exclusive.
Forgetting deleteFile=true for a temporary, per-request generated file
Without it, a file generated just to be downloaded once stays on disk indefinitely, quietly accumulating over time.
Best Practices
- Validate a requested file identifier against a database or allowlist, never build the actual server path from raw user input.
- Set Content-Disposition explicitly, inline or attachment, rather than relying on the browser's default handling for the MIME type.
- Use deleteFile=true when the file being served was generated just for this one download.
- Set an accurate MIME type with the type attribute, falling back to application/octet-stream only when the real type genuinely isn't known.
Interview Questions
What's the difference between Content-Disposition: inline and attachment?
inline lets a browser capable of rendering the MIME type display it directly. attachment always forces a "Save As" download prompt, regardless of what the browser could otherwise render.
Why is building a download's file path directly from a URL parameter risky?
It opens the door to a path-traversal request. The safer pattern validates an identifier against a database or allowlist, and only builds the real file path from that trusted, server-side value.
What happens to page processing once cfcontent's file or variable attribute is set?
It halts further processing, similar to cfabort, nothing after it on the page runs.
Why would cfheader throw an error in some cases?
Calling it after cfflush has already sent output to the client throws an error, cfheader has to run before that point.
Summary
In this lesson, you forced a file download with cfheader's Content-Disposition and cfcontent, chose deliberately between inline and attachment delivery, avoided the real path-traversal risk of building a file path from untrusted input, and covered Lucee's range attribute for resumable downloads.
What's Next?
The next lesson covers encoding: the EncodeForHTML/EncodeForURL/EncodeForJavaScript family, and closing a real gap left open here, non-ASCII filenames in a Content-Disposition header.