MP4视频经Safari→Cloudflare→nginx→Rails链路无法播放的技术求助
解决Safari视频播放失败:Cloudflare下的Range请求与206响应问题
你分析得很对——Safari对视频播放的要求确实严格,必须支持Range请求并返回206部分内容响应,而你的问题核心大概率出在Cloudflare没有把Range请求头传递到Rails服务器,导致服务器只能返回200,进而让Safari拒绝播放。下面是一步步的解决方案:
1. 先搞定Cloudflare的Range请求配置
Cloudflare默认可能会拦截或修改Range请求(尤其是缓存视频时),你需要确保它把Range头完整传递给后端,并且保留服务器返回的206响应:
- 登录Cloudflare控制台,进入你的域名管理页面
- 转到「规则」→「页面规则」,创建一条新规则:
- 匹配路径:比如
*yourdomain.com/videos/*(根据你实际的视频路由调整) - 添加以下设置:
- Range Requests: 设为「On」(这个选项会让Cloudflare支持Range请求,且不修改相关头信息)
- 缓存级别: 可以设为「标准缓存」,如果视频经常更新,也可暂时设为「不缓存」测试
- Auto Minify: 确保对「视频」选项关闭(避免Cloudflare修改视频文件)
- 匹配路径:比如
- 保存规则后,记得清除Cloudflare的缓存(在「缓存」→「配置」里点击「清除全部缓存」,或只清除对应视频文件的缓存)
2. 验证Range请求是否真的到达服务器
你之前说导出请求头没看到Range,得确认是Cloudflare没传,还是其他环节丢了。在Rails控制器里加一行日志:
def videos # 新增日志,记录Range头 Rails.logger.info "Incoming Range header: #{request.headers['Range']}" file_name = params.fetch(:filename, '') file_path = "#{Rails.root}/app/assets/videos/#{file_name}.#{params.fetch(:extension, '')}" raise ActionController::RoutingError, 'Not Found' unless file_name.index('/').nil? && File.exist?(file_path) # 新增Accept-Ranges头,告诉客户端服务器支持分段请求 response.headers['Accept-Ranges'] = 'bytes' send_file(file_path, type: 'video/mp4', disposition: 'inline', status: (request.headers['Range'].present? ? 206 : 200)) end
用Safari访问视频后查看Rails日志:如果还是看不到Range头,说明Cloudflare配置没生效或有其他规则拦截;如果能看到,再往下排查。
3. 优化Rails的206响应处理
虽然你的代码逻辑没问题,但Rails的send_file在某些场景下可能没正确生成Content-Range头,手动补充会更保险:
def videos Rails.logger.info "Incoming Range header: #{request.headers['Range']}" file_name = params.fetch(:filename, '') file_path = "#{Rails.root}/app/assets/videos/#{file_name}.#{params.fetch(:extension, '')}" raise ActionController::RoutingError, 'Not Found' unless file_name.index('/').nil? && File.exist?(file_path) response.headers['Accept-Ranges'] = 'bytes' file_size = File.size(file_path) if request.headers['Range'].present? # 解析Range头,比如 "bytes=0-" 或者 "bytes=100-200" range_match = request.headers['Range'].match(/bytes=(\d+)-(\d*)/) start_byte = range_match[1].to_i end_byte = range_match[2].present? ? range_match[2].to_i : file_size - 1 # 确保end_byte不超过文件大小 end_byte = [end_byte, file_size - 1].min # 设置Content-Range和Content-Length头 response.headers['Content-Range'] = "bytes #{start_byte}-#{end_byte}/#{file_size}" response.headers['Content-Length'] = (end_byte - start_byte + 1).to_s send_file(file_path, type: 'video/mp4', disposition: 'inline', status: 206, range: "#{start_byte}-#{end_byte}") else send_file(file_path, type: 'video/mp4', disposition: 'inline', status: 200) end end
这里手动解析了Range头,并且在send_file里指定了range参数,确保Rails正确返回部分内容。
4. 检查Nginx的转发配置
你说Nginx只是转发到Puma,但要确保它没有清空Range请求头。打开Nginx配置文件,找到对应server块的proxy_pass部分,添加以下指令:
location /videos { proxy_pass http://localhost:3000; # 你的Puma地址 # 确保传递Range和If-Range头 proxy_set_header Range $http_range; proxy_set_header If-Range $http_if_range; # 保留其他必要的请求头 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
修改后重启Nginx,确保配置生效。
最后测试
完成以上配置后,用Safari打开视频,同时查看Rails日志:
- 如果日志里能看到Range头,且服务器返回206状态码,视频应该能正常播放了
- 如果还是不行,检查Safari开发者工具(网络面板):
- 确认响应头有
Accept-Ranges: bytes - 确认Range请求返回206状态码,且有
Content-Range头
- 确认响应头有
内容的提问来源于stack exchange,提问作者Jason
相关产品推荐
相关产品推荐

