PAYATHON 2026

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 官方文档

先检查状态码、响应头和文件内容

  1. 在普通浏览器中打开同一个 URL,确认该日期的文件存在,并观察是否经过跳转或验证。
  2. 记录 Apps Script 收到的 HTTP 状态码,以及 Content-Type、cf-mitigated 和 cf-ray 等响应头。
  3. 必要时临时设置 followRedirects: false,检查第一跳响应及其 Location,确认是否跳转到了错误页。
  4. 将请求时间、完整 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,无法验证文件是否完整、内容是否正确。

如果网站要求正常的登录会话、特定请求头,或公开下载流程中的会话 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 或临时服务错误时,按响应要求进行有限重试。下载失败后应保留旧数据并标明日期,避免将旧汇率当作当天数据使用。

备注:内容仅供参考。