花拾录
← 返回知识库

匿名调用代码托管平台 API 批量下载,很快就被限流

后端开发导入2026/09/220 阅读0 评论

批量下载代码仓库的时候,前几十个正常,跑着跑着突然全部变成 403。你以为是被封了 IP,其实是撞上了一个大多数人根本不知道的配额限制。

现象

调某代码托管平台的 API 批量下载文件,中途开始全部返回 403:

请求 1  ~ 58:  200 OK
请求 59 ~ 62:  403 Forbidden
请求 63 ~ ...: 403 Forbidden

响应体里通常带着明确的字样:

{
  "message": "API rate limit exceeded for <IP>. ...",
  "documentation_url": ".../rest/overview/resources-in-the-rest-api#rate-limiting"
}

报错信息里明确写着 rate limit。奇怪的是,如果你停下来等一会儿,又能继续下载一小批,然后再次 403——这个"周期性恢复、很快又耗尽"的节奏,就是配额限制的典型表现。

有一个细节能一眼区分"被限流"和"被拉黑":响应头里的限额字段。

curl -sI https://<api_host>/repos/<owner>/<repo> | grep -i ratelimit
# X-RateLimit-Limit: 60
# X-RateLimit-Remaining: 0
# X-RateLimit-Reset: 1720000000

Remaining 归零、Reset 是一个未来时间戳——这就确诊了,不是权限问题,是额度用光。

根因

匿名访问该 API 有每小时 60 次的配额。 批量下载会很快把这 60 次耗光,之后所有请求都被拒绝,直到配额重置。

这背后的逻辑是:平台对未认证的请求给出了一个很小的免费额度,鼓励你用认证方式访问。所以 403 不是"你被拉黑了",而是"你这个小时的动作次数用完了"。理解了这一点,解决办法就清晰了:要么减少 API 调用次数,要么用带凭证的方式获得更高配额。

还有两个容易忽略的点。其一,配额通常按来源 IP 计,如果你处在 NAT 或代理后面,同一条出口上的所有人共享这 60 次,别人先耗光了你也会被连累。其二,每次 API 调用只算一次,不管返回的内容大小——所以一个"先列目录、再逐个下载"的实现,会立刻把配额打满。

解决

三条路,按推荐程度排序:

第一,改用不占用 API 配额的原始文件域名(raw 域名)。 这是批量下载最实用的办法。文件原始内容的访问通常走的是 CDN,不计入 API 配额:

# 不要走 API 接口(会消耗配额)
# api_host /repos/<owner>/<repo>/contents/<path>

# 改走 raw 域名(通常不占用 API 配额)
raw_url = f"https://raw.<host>/{owner}/{repo}/{branch}/{path}"
resp = requests.get(raw_url, timeout=15)
resp.raise_for_status()

第二,申请带凭证的访问。 用 token 认证之后,配额通常会有数量级的提升:

HEADERS = {"Authorization": "Bearer <API Key>"}
resp = requests.get(api_url, headers=HEADERS, timeout=15)
resp.raise_for_status()

注意,即使有了 token,也要配合节流和重试,别把提升后的配额也一次打光。凭证要放进环境变量,不要写死在代码里。

第三,等待配额重置。 如果只是偶发需求,等一小时自然恢复。但这对批量任务来说不现实——一个需要下载几百个对象的任务,靠等待会拖到天亮。

综合起来,批量下载的最佳实践是:优先走 raw 域名,需要元数据时才走 API,并且给 API 调用加重试和退避。

import time, requests

def get_with_backoff(url, tries=5, **kw):
    for i in range(tries):
        r = requests.get(url, timeout=15, **kw)
        if r.status_code in (403, 429):
            wait = int(r.headers.get("Retry-After", 2 ** i))
            time.sleep(wait)
            continue
        r.raise_for_status()
        return r
    raise RuntimeError("quota exhausted")

延伸与预防

这个坑的通用教训是:看清 API 配额,能用"非 API 通道"的就别走 API。

平台通常会把"公开文件内容访问"和"API 调用"分成两条通道,前者免费且量大,后者受配额约束。要学会辨认这个区别。预防措施:

  • 动手前先读配额文档:每小时多少次、匿名和认证各是多少、超限返回什么状态码;
  • 优先选择低配额消耗的通道:raw / CDN 域名用于内容,API 只用于必须的元数据查询;
  • 给请求加计数,接近配额上限时主动降速或暂停,而不是硬撞到 403;
  • 对 403 / 429 做指数退避,别在配额耗尽时疯狂重试——那只会让情况更糟;
  • 把凭证用起来并放进环境变量,认证通道的额度通常足够跑完一整批。

最后,状态码本身也会误导人。403 通常被理解为"权限不足",但在配额场景下它实际表示"频率超限";而 429 才是语义上正确的"请求过多"。看到 403 时,先看响应体和响应头里有没有 rate limit / X-RateLimit-Remaining 之类的字段,再决定是去查权限还是去降速。

评论(0)

  • 还没有评论,来抢沙发~

相关文章