UrlFetchApp 下载 PDF 时返回 Cloudflare 拦截页面,如何处理?
结论
UrlFetchApp 没有通用参数可以绕过 Cloudflare 拦截。如果站点要求完成浏览器挑战,或明确拒绝脚本的出口 IP,仅修改 User-Agent、Accept 或添加 Cookie,通常无法稳定解决问题。
应先确认拦截类型,再联系网站申请自动化下载接口或访问放行。需要中转时,可以使用固定出口 IP 的下载服务,但这个出口也必须获得目标站点的访问许可。使用代理或 scraping API 并不保证成功。
现有日志只能说明请求收到了拒绝访问页面,无法证明 Google 的 IP 已被列入黑名单,也无法确定网站在 4 月 9 日修改了哪条规则。
为什么请求 PDF 会返回 HTML
URL 以 .pdf 结尾,不代表响应内容一定是 PDF。触发访问控制时,服务器或前置安全服务可以直接返回 HTML 错误页。
Cloudflare 的 Challenge Page 会返回 text/html,并带有 cf-mitigated: challenge 响应头。直接拒绝访问的页面和浏览器挑战页需要区分,不能把所有 Access Denied 都当成执行 JavaScript 就能解决的挑战。Cloudflare 响应识别说明
页面中出现 static.cloudflareinsights.com/beacon.min.js,只能说明页面引用了 Cloudflare 的统计脚本,无法据此判断是哪一层安全规则拦截了请求。可以将 Signature ID 和 Message ID 提供给网站管理员,由管理员结合日志查询。
UrlFetchApp 使用 Google 的网络基础设施,从一定范围的 IP 地址发出请求。是否因为 Google 出口而被封禁,仍需通过站点日志或对照测试确认。UrlFetchApp 官方文档
先检查状态码、响应头和文件内容
- 在普通浏览器中打开同一个 URL,确认该日期的文件存在,并观察是否经过跳转或验证。
- 记录 Apps Script 收到的 HTTP 状态码,以及
Content-Type、cf-mitigated和cf-ray等响应头。 - 必要时临时设置
followRedirects: false,检查第一跳响应及其Location,确认是否跳转到了错误页。 - 将请求时间、完整 URL、状态码和页面中的事件编号提供给网站管理员。
浏览器能下载、Apps Script 不能下载,只能说明网站对两种访问环境的处理结果不同,单凭这一点还不能认定是 IP 封禁。
以下代码用于诊断响应,防止错误页面进入后续 PDF 处理流程,不含绕过拦截的逻辑:
function testDownload() {
const url =
"https://www.sbp.org.pk/ecodata/rates/war/2026/May/05-May-26.pdf";
const response = UrlFetchApp.fetch(url, {
muteHttpExceptions: true,
followRedirects: true,
headers: {
Accept: "application/pdf"
}
});
const headers = response.getAllHeaders();
// HTTP 响应头名称不区分大小写。
function header(name) {
const key = Object.keys(headers).find(
key => key.toLowerCase() === name.toLowerCase()
);
if (!key) return "";
const value = headers[key];
return Array.isArray(value) ? value.join(", ") : String(value);
}
const status = response.getResponseCode();
const contentType = header("Content-Type");
const mime = contentType.split(";")[0].trim().toLowerCase();
const mitigation = header("cf-mitigated").toLowerCase();
Logger.log(JSON.stringify({
status: status,
contentType: contentType,
cfMitigated: mitigation,
cfRay: header("cf-ray"),
location: header("Location")
}));
const bytes = response.getContent();
const signature = bytes
.slice(0, 5)
.map(byte => String.fromCharCode(byte & 0xff))
.join("");
const acceptableMime =
mime === "application/pdf" ||
mime === "application/octet-stream" ||
mime === "";
if (
status !== 200 ||
mitigation === "challenge" ||
!acceptableMime ||
signature !== "%PDF-"
) {
if (mime === "text/html") {
Logger.log(response.getContentText().substring(0, 300));
}
throw new Error(
"未取得预期 PDF:HTTP " + status +
",Content-Type=" + contentType
);
}
return response.getBlob().setName("05-May-26.pdf");
}
HTTPResponse 提供读取状态码、响应头和原始字节的方法。成功取得 PDF 后,应保留其字节或 Blob,避免转成文本后再保存。HTTPResponse 官方文档
代码允许 application/octet-stream,以兼容服务器使用通用二进制类型的情况。%PDF- 检查只能初步识别 PDF,无法验证文件是否完整、内容是否正确。
Headers 或 Cookie 什么时候有用
如果网站要求正常的登录会话、特定请求头,或公开下载流程中的会话 Cookie,按要求构造请求可能有效。应先确认网站的访问要求,再调整参数。
Cloudflare 浏览器挑战还涉及以下限制:
- 修改
User-Agent只会改变请求头,无法让UrlFetchApp具备浏览器的页面执行环境。 UrlFetchApp不会执行返回 HTML 中的挑战脚本。Cloudflare 明确说明,挑战机制不支持缺少 JavaScript 执行能力的客户端。Cloudflare 支持环境说明cf_clearance表示访问者已完成验证,并与签发时的访问者及设备关联。将它从本地浏览器复制到 Apps Script,无法作为可靠的长期方案。Cloudflare Clearance 说明
muteHttpExceptions: true 让代码能够接收并检查 HTTP 错误响应;Accept: application/pdf 表达期望的响应类型。两者都不会让安全规则自动放行。UrlFetchApp 参数说明
长期下载应采用什么架构
对于每天获取一次的公开汇率表,应优先联系网站,询问是否有官方 API、批量下载入口或数据订阅渠道,以及能否为自动化访问配置放行规则。这些渠道是否存在,需要由网站确认。
如果网站愿意按固定 IP 放行,而 Apps Script 的共享出口不便管理,可以部署一个小型下载服务:
Apps Script
→ 带身份验证的下载接口
→ 固定出口 IP 的下载服务
→ 目标网站
下载服务
→ 校验 PDF 并按日期缓存
→ 向 Apps Script 返回 PDF 二进制
中转服务需要配置固定出口,部署到云函数或容器平台并不代表自动拥有固定 IP。将同一段请求迁到另一家云平台,也可能遇到相同的拦截。
UrlFetchApp 没有原生的 HTTP/SOCKS 代理配置参数。使用中转时,应请求中转服务的 HTTPS 接口,由服务端下载目标文件。
| 已确认的情况 | 处理方式 |
|---|---|
| URL、日期或重定向有误 | 修正下载地址或解析流程 |
| 缺少网站要求的普通会话信息 | 按网站规定维护会话 |
| Apps Script 出口被拒绝,网站允许固定 IP 访问 | 使用固定出口下载服务并申请放行 |
| 站点要求浏览器挑战 | 向网站申请适合自动化访问的入口 |
| 考虑使用 scraping API | 先验证目标 URL、PDF 二进制支持和持续可用性,再决定是否采用 |
如果网站正常的下载流程需要浏览器自动化,可以在外部服务运行浏览器,但无法保证它能通过 Cloudflare 挑战。Cloudflare 明确表示,生产环境的挑战解题流程不支持浏览器自动化框架。Cloudflare 支持环境说明
运行时注意事项
下载服务和 Apps Script 都应验证响应,防止中转服务返回 HTTP 200,实际内容却仍是 HTML。修改 MIME 类型或将文件改名为 .pdf,都不能说明下载成功。
每日文件应按日期缓存。遇到明确的拒绝访问时,记录并告警;遇到 429 或临时服务错误时,按响应要求进行有限重试。下载失败后应保留旧数据并标明日期,避免将旧汇率当作当天数据使用。
备注:内容仅供参考。
