實作圖片下載功能時,點擊下載連結後,瀏覽器下載的圖片檔案無法開啟(損壞、大小為 0、或開啟為亂碼),但檔案路徑確認正確、readfile() 邏輯也沒錯。
在呼叫 header() 之前已有輸出內容(空白字元、BOM、Echo、錯誤訊息、Include 檔案頭尾的空行等)。HTTP 協定要求 Header 必須在 Body 之前發送,任何提前輸出會導致:
Cannot modify header information - headers already sent)<?php
// ❌ 錯誤:這裡有一個換行或空白就會導致輸出
$filename = $_GET["photo"];
// 沒有清除緩衝區
header("Content-Type: image/jpeg");
header("Content-Disposition: attachment; filename=" . $filename);
header("Content-Length: " . filesize($filename));
readfile($filename);
<?php
$filename = $_GET["photo"];
// 關鍵:清除之前所有輸出緩衝
ob_clean();
header("Content-Type: image/jpeg");
header("Content-Disposition: attachment; filename=" . basename($filename));
header("Content-Length: " . filesize($filename));
readfile($filename);
exit; // 確保無後續輸出
<?php
ob_start(); // 放在檔案最第一行,甚至在任何 HTML/空白之前
// ... 其餘邏輯
header(...);
readfile(...);
<?php 緊貼檔案開頭,結尾 ?> 可省略(純 PHP 檔案建議省略)<?php
/**
* 安全的檔案下載函數
* @param string $filePath 伺服器絕對路徑
* @param string $downloadName 下載時顯示的檔名(可選)
* @param string $mimeType MIME 類型(可選,自動偵測)
*/
function downloadFile($filePath, $downloadName = '', $mimeType = '') {
// 1. 基礎驗證
if (!is_file($filePath) || !is_readable($filePath)) {
http_response_code(404);
exit('檔案不存在或不可讀');
}
// 2. 防止目錄遍歷攻擊
$realBase = realpath('/var/www/uploads'); // 允許的根目錄
$realFile = realpath($filePath);
if (strpos($realFile, $realBase) !== 0) {
http_response_code(403);
exit('存取被拒絕');
}
// 3. 清除輸出緩衝
if (ob_get_level()) ob_clean();
// 4. 設定 Header
$downloadName = $downloadName ?: basename($filePath);
// RFC 5987 編碼檔名,支援中文
$encodedName = rawurlencode($downloadName);
header("Content-Type: " . ($mimeType ?: mime_content_type($filePath)));
header("Content-Disposition: attachment; filename*=UTF-8''{$encodedName}");
header("Content-Length: " . filesize($filePath));
header("Content-Transfer-Encoding: binary");
header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
header("Pragma: public");
header("Expires: 0");
// 5. 輸出檔案內容
$handle = fopen($filePath, 'rb');
while (!feof($handle) && connection_status() === 0) {
echo fread($handle, 8192);
flush();
}
fclose($handle);
exit;
}
// 使用範例
$photo = $_GET['photo'] ?? '';
$safePath = '/var/www/uploads/' . basename($photo); // 簡單防遍歷
downloadFile($safePath);
| 現象 | 可能原因 | 解決 |
|---|---|---|
| 下載檔案開頭有 HTML/空白 | Header 前有 Echo/空白/BOM | ob_clean() / 移除 BOM / 檔案開頭無空白 |
| 中文檔名下載亂碼 | Content-Disposition 編碼錯誤 | 使用 filename*=UTF-8''+rawurlencode() |
| 大檔案下載中斷/記憶體爆滿 | readfile() 一次讀入記憶體 | 分塊 fread+flush / X-Sendfile / Nginx X-Accel |
| IE 下載彈出「無法下載」 | Cache-Control 衝突 / HTTPS 下 Pragma:no-cache | 調整 Cache/Pragma Header |
ob_clean() 是解決「Header 前輸出」最快的救命稻草,但根治之道是**養成良好習慣**:PHP 檔案無 BOM、純 PHP 檔案省略結尾標籤、入口腳本開頭 ob_start()、錯誤訊息走日誌不直接輸出。下載功能更要注意大檔案分塊傳輸、檔名編碼、路徑安全、MIME 偵測等細節,才能經得起生產環境考驗。