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

PHP流式输出PDF引发Safari标签崩溃问题求助

Safari下PHP流式输出PDF导致标签崩溃的问题排查与解决方案

问题场景

无框架PHP脚本通过唯一ID查询数据库获取PDF路径,使用readfile()流式输出文件,配合前端JS动态创建object/embed/iframe标签展示PDF(文件均小于750KB)。该方案在Chrome、Firefox中正常,但在Safari 16.5及旧版本中,点击几次PDF链接后标签会崩溃挂起,仅能通过重启Safari恢复。

观察到的异常行为:

  • Safari会对同一PHP脚本创建多个连接,部分连接下载几KB后断开
  • 切换PDF链接时,Safari会携带Range头发起部分请求,触发报错并导致标签挂起

疑问解答

1. 为何Safari会创建多个连接?

Safari的WebKit引擎在处理嵌入式PDF时,存在特殊的资源加载策略:

  • 动态重建object/embed标签时,旧的请求可能未被正确终止,导致残留连接
  • WebKit会发起并行请求进行资源预加载或校验,与Chrome/Firefox的连接复用策略不同
  • 响应头中的缓存控制或Last-Modified字段若存在歧义,会触发重复请求

2. Accept-Ranges: none是否被Safari忽略?

是的,Safari(尤其是16.5及旧版本)存在忽略该响应头的已知兼容性Bug。即使明确声明不支持范围请求,它仍可能强制发送Range头发起部分请求。

3. 处理Range头能否解决问题?

能,这是解决该问题的核心方向。Safari崩溃的本质是:它发起Range请求后,后端返回了完整的200响应而非符合HTTP规范的206部分响应,导致浏览器请求处理逻辑混乱,最终引发标签挂起。正确处理Range头并返回206响应,即可修复该问题。


可靠解决方案

1. 完善PHP脚本的Range请求处理

替换原脚本中的响应头和文件输出逻辑,添加完整的Range请求处理:

// ... 保留原有的数据库查询、文件检查逻辑 ...

$fileSize = filesize($fileFullPath);

// 替换废弃的mime_content_type,使用更可靠的finfo
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($fileFullPath);

// 处理Range请求
$range = isset($_SERVER['HTTP_RANGE']) ? $_SERVER['HTTP_RANGE'] : false;
if ($range) {
    list($param, $rangeStr) = explode('=', $range, 2);
    if (strtolower(trim($param)) !== 'bytes') {
        http_response_code(416);
        exit();
    }
    
    list($start, $end) = explode('-', $rangeStr);
    $start = (int)$start;
    $end = $end !== '' ? (int)$end : $fileSize - 1;
    
    // 校验Range的合法性
    if ($start >= $fileSize || $end >= $fileSize || $start > $end) {
        http_response_code(416);
        exit();
    }
    
    $length = $end - $start + 1;
    // 发送206部分响应头
    header('HTTP/1.1 206 Partial Content');
    header("Content-Range: bytes {$start}-{$end}/{$fileSize}");
} else {
    $start = 0;
    $end = $fileSize - 1;
    $length = $fileSize;
    // 改为声明支持范围请求,避免Safari强制发起错误请求
    header('Accept-Ranges: bytes');
}

// 通用响应头设置
header("Content-Type: {$mime}");
header("Content-Length: {$length}");
// 修正Content-Disposition的引号转义问题
header("Content-Disposition: inline; filename=\"{$fileRow['filename']}\"");
header('Content-Transfer-Encoding: binary');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', filemtime($fileFullPath)) . ' GMT');
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 86400) . ' GMT');
header('Cache-Control: max-age=86400');

// 流式输出指定范围的文件内容
$fileHandle = fopen($fileFullPath, 'rb');
fseek($fileHandle, $start);

while (!feof($fileHandle) && (ftell($fileHandle) <= $end)) {
    $remaining = $end - ftell($fileHandle) + 1;
    $chunkSize = min(8192, $remaining);
    echo fread($fileHandle, $chunkSize);
    flush();
}

fclose($fileHandle);
exit();

2. 前端优化建议

  • 动态创建PDF容器标签时,确保旧标签被彻底销毁(调用element.remove()),避免残留的未终止请求
  • 避免频繁重建object/embed标签,可考虑复用标签并修改data属性

内容的提问来源于stack exchange,提问作者Delta Sierra

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 07:55:11