批量下载代码仓库的时候,前几十个正常,跑着跑着突然全部变成 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 之类的字段,再决定是去查权限还是去降速。