You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PHP通过readfile()提供文件时的客户端缓存问题排查

问题

在Apache服务器上通过PHP脚本向已认证用户提供存储在web根目录外的PDF文件,认证通过后用readfile()传输文件。因为文件体积较大,希望客户端浏览器能本地缓存以节省带宽,已设置缓存响应头,但仅Safari生效,Firefox和Chrome均无效,求排查遗漏点。

代码与响应头

脚本代码

<?php

if(!defined('BASE_URI')){
    require_once('../includes/config.inc.php');
    require_once('../includes/utils.inc.php');
    trigger_error("unauthorized direct module access");
    print_json_error("something went wrong, we are working on it...");
    exit();
}

require_once('../includes/utils.inc.php');
require_once(DB);

$auth = new \Delight\Auth\Auth($conn);

if (!$auth->isLoggedIn()) {
    trigger_error("NOT LOGGED IN");
    print_json_error("NOT LOGGED IN");
    exit;
}

if (!$auth->hasRole(\Delight\Auth\Role::SUPER_ADMIN)){
    trigger_error("WRONG CREDENTIALS");
    print_json_error("WRONG CREDENTIALS");
    exit;
}

if(getenv('REQUEST_METHOD') == 'GET'){

    
    if(!isset($_GET['mime']) || !isset($_GET['path'])){

        trigger_error("wrong input: ".$_GET);
        print_json_error("wrong input");
        exit();
    }

    $filepath = '../restricted/'.$_GET['path'];
    $path = getcwd().$filepath;
    $pathdir = dirname($path);
    $checkPath = realpath(pathinfo($path)['dirname']);

    if(strpos($pathdir, $checkPath) !== 0 || strpos($pathdir, $checkPath) === false) { 
        trigger_error("INVALID GET PATH: ".$checkPath." ".$pathdir);
        print_json_error("INVALID GET PATH");
        exit;
    }

    if($_GET['mime'] == 'pdf'){
        header('Content-type: application/pdf');
        header("Content-type: application/octet-stream");
        header("Content-Disposition: attachment; filename=$filepath");
        
        $seconds_to_cache = 360000;
        $ts = gmdate("D, d M Y H:i:s", time() + $seconds_to_cache) . " GMT";
        header("Expires: $ts");
        header("Pragma: cache");
        header("Cache-Control: max-age=$seconds_to_cache, immutable");
        
        
        ob_end_flush();
        readfile($filepath);
        exit();
    } else {
        trigger_error("UNSUPPORTED FILE TYPE: ".$_GET['mime']);
        print_json_error("UNSUPPORTED FILE TYPE");
        exit();
    }

}

trigger_error("INVALID REQUEST");
print_json_error("INVALID REQUEST");
exit();

?>

响应头信息

HTTP/2 200 OK
content-type: application/octet-stream
date: Thu, 28 Jul 2022 13:21:35 GMT
server: Apache
x-frame-options: sameorigin
x-content-type-options: nosniff
content-disposition: attachment; filename=[..]
expires: Mon, 01 Aug 2022 17:21:35 GMT
pragma: cache
cache-control: max-age=360000, immutable
X-Firefox-Spdy: h2
问题排查与解决办法
  • Content-Type冲突导致缓存策略限制:脚本先后设置application/pdf和application/octet-stream,最终响应头仅保留后者。Chrome和Firefox对octet-stream这类通用二进制流的缓存策略更严格,默认倾向于不缓存。需只保留application/pdf,让浏览器识别文件类型,触发正常缓存逻辑。
  • 缺少缓存验证标识头:Chrome和Firefox需要ETag或Last-Modified来确认文件是否变更,进而决定是否使用缓存。需添加这两个头,并处理浏览器发送的If-None-Match/If-Modified-Since请求,返回304状态码直接复用缓存。
  • Content-Disposition的filename格式问题:当前filename使用相对路径../restricted/xxx.pdf,路径字符可能干扰浏览器的缓存键识别,建议只保留纯文件名(如xxx.pdf)。
  • 缓冲区处理不彻底:ob_end_flush()可能未完全清理缓冲区,残留内容可能导致响应异常,影响缓存判断。改用ob_clean(); flush();确保缓冲区干净后再输出文件。

修改后的关键代码片段

if($_GET['mime'] == 'pdf'){
    // 只保留正确的PDF类型标识
    header('Content-type: application/pdf');
    // 提取纯文件名作为下载标识
    $filename = basename($filepath);
    header("Content-Disposition: attachment; filename=$filename");
    
    $file_mtime = filemtime($filepath);
    $seconds_to_cache = 360000;
    $ts_expires = gmdate("D, d M Y H:i:s", time() + $seconds_to_cache) . " GMT";
    $ts_modified = gmdate("D, d M Y H:i:s", $file_mtime) . " GMT";
    
    // 基于文件修改时间和大小生成唯一ETag
    $etag = md5($file_mtime . filesize($filepath));
    
    header("Expires: $ts_expires");
    header("Last-Modified: $ts_modified");
    header("ETag: \"$etag\"");
    // 添加public标识,允许浏览器缓存
    header("Cache-Control: public, max-age=$seconds_to_cache, immutable");
    
    // 彻底清理缓冲区
    ob_clean();
    flush();
    
    // 处理浏览器缓存验证请求,直接返回304复用缓存
    if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] == "\"$etag\"") {
        header("HTTP/1.1 304 Not Modified");
        exit();
    }
    if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) >= $file_mtime) {
        header("HTTP/1.1 304 Not Modified");
        exit();
    }
    
    readfile($filepath);
    exit();
}

内容的提问来源于stack exchange,提问作者fayong lin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.24 10:24:12