DevLearningTools

MODULE 13 · LESSON 04

File Downloads

Serving a file to the browser with cfcontent and cfheader, choosing inline vs attachment with Content-Disposition, avoiding a path-traversal mistake, and a real Lucee-specific range attribute for resumable downloads.

New lessons are added one at a time as the course gets built out — a graded quiz for each lesson is still on the way.

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

Tag Syntax
<cfheader name="Content-Disposition" value="attachment; filename=report.pdf">
<cfcontent type="application/pdf" file="/var/data/reports/report-2026.pdf">
CFScript
cfheader(name = "Content-Disposition", value = "attachment; filename=report.pdf");
cfcontent(type = "application/pdf", file = "/var/data/reports/report-2026.pdf");
NOTE

Content-Disposition: attachment is what forces a "Save As" dialog instead of the browser trying to render the file inline.

cfcontent's Attributes

AttributeMeaning
typeThe MIME type of the response (e.g. application/pdf, application/vnd.ms-excel)
fileA server-side file path whose contents become the page output
variableA binary variable (like a generated PDF or chart) to output instead of a file
deleteFileDeletes the file from disk after it's sent (default no)
resetDiscards output rendered so far (default yes); has no effect when file is set
NOTE

Once file or variable is specified, cfcontent halts further page processing, similar to cfabort, nothing after it on the page runs.

cfheader's Attributes

AttributeMeaning
nameThe header name to send (required unless statusCode is used)
valueThe header's value
statusCode / statusTextSets an HTTP status code instead of (or alongside) a named header
charsetCharacter encoding for the header value, defaults to UTF-8
NOTE

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

Tag Syntax — Render in the Browser
<cfheader name="Content-Disposition" value="inline; filename=invoice.pdf">
<cfcontent type="application/pdf" file="#invoicePath#">
Tag Syntax — Force a Download
<cfheader name="Content-Disposition" value="attachment; filename=invoice.pdf">
<cfcontent type="application/pdf" file="#invoicePath#">
NOTE

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

Tag Syntax — Validated Against a Known Set
<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#">
NOTE

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

NOTE

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.