开发者指南

如何用 cURL 跟随并检查重定向

使用 curl -L 跟随 HTTP 重定向,检查响应头链、最终 URL 和请求方法,限制循环并核对代理与鉴权行为。

302 将 POST 变为 GET,307 保留 POST 和示例正文的观测结果

概览

操作流程

  1. 检查首个响应

    先查看状态和 Location,再决定是否跟随重定向链。

  2. 跟随后检查最终请求

    使用 -L 并限制跳转次数,核对最终 URL、方法和正文。

使用 curl --location URL,或简写 curl -L URL,可以跟随 HTTP 重定向。在把返回正文当成预期页面之前,应增加重定向次数上限,并检查最终 URL。默认情况下,curl 只返回初始响应,不会跟随其中的 Location 响应头。

对于采集和地区测试,最终状态成功只说明了一部分情况。重定向链可能结束于登录页、地区首页,或其他主机上一个并非解析器预期的页面。

跟随重定向链,并保留检查依据

将 TARGET_URL 替换为起始地址。下面的 macOS/Linux 命令分别保存响应头和正文,并输出最终目标:

curl --location --max-redirs 5 --max-time 30 \
  --silent --show-error --dump-header redirect-headers.txt \
  --output response.html \
  --write-out 'final=%{url_effective} status=%{http_code} redirects=%{num_redirects}\n' \
  'TARGET_URL'

响应头文件包含重定向链中遇到的响应。将每个状态与对应的 Location 值一起检查,然后查看最终正文。响应头文件可能包含 Cookie,分享前应移除敏感字段。Windows 请使用 curl.exe,并将命令写在一行。

如果只想检查初始响应而不跟随重定向,可以省略 --location。--head 会发起 HEAD 请求,并不只是隐藏 GET 请求的正文。如果服务器对 HEAD 的处理不同,应使用 GET 并丢弃其正文。详见 curl 重定向指南。

检查 POST 重定向后的请求方法

302 将 POST 变为 GET,307 保留 POST 和示例正文的观测结果
随附原图记录:本地 curl 8.7.1 使用 -L --data demo=1;302 转为 GET,307 保留 POST 与正文。

素材图记录了 curl 8.7.1 在本地运行 -L --data demo=1 的结果:302 后使用 GET;307 保留 POST 和请求正文。

响应

对使用 --data 创建的 POST,--location 通常如何处理

301 或 302

默认将后续请求改为 GET

303

后续请求通常改为 GET

307 或 308

保留请求方法和正文

不要仅仅为了发送数据而添加 -X POST。显式指定的自定义方法,可能产生与 curl 正常自动选择方法时不同的重定向行为。只有目标 API 的约定要求时,才使用保留请求方法的选项,并在接收端验证结果。

重定向到其他主机时,应把它当成新的目标。默认情况下,curl 会限制标准鉴权凭据跨主机转发;--location-trusted 会放宽这项保护。X-API-Key 等自定义敏感请求头尤其需要谨慎。请检查目标;如果凭据只属于一个源站,就不要盲目跟随重定向。

在本地比较不跟随、跟随和循环重定向

启动随附的 network_demo.py。它的路由返回可控响应,不会发起外部请求。

curl --noproxy '*' --silent --output /dev/null \
  --write-out '%{http_code}\n' 'http://127.0.0.1:8766/redirect'

curl --noproxy '*' --silent --location --max-redirs 5 \
  'http://127.0.0.1:8766/redirect'

curl --noproxy '*' --location --max-redirs 2 \
  'http://127.0.0.1:8766/loop'

第一条命令输出 302。第二条到达 /headers 的 JSON 响应。第三条因为重定向次数超过上限而停止,curl 退出码为 47。这些本地结果可以帮助你区分循环重定向和连接超时。

同一演示服务还提供 /redirect302 和 /redirect307,用于比较 POST 行为。配合 --location 发送 --data 'demo=1':前者的最终响应报告 GET,后者报告 POST。此测试只使用演示数据。

保持重定向链中的路由条件一致

通过 MIYAIP 测试时,在相同命令中添加分配的代理字段。确认适用的会话策略,并检查每个目标。代理端点字符串保持不变,本身不能证明轮换服务在每次请求中都维持同一出口。

比较直连和代理结果时,应使用相同的起始 URL、方法、请求头和重定向策略。差异可能来自语言、地区、鉴权或目标规则,不能自动认定为代理故障。浏览器中的 IP 与浏览器环境检查报告该浏览器观察到的结果,而 curl 是另一种客户端。

如果设置了明确的客户端标识,先设置并验证 cURL User-Agent,再在比较重定向行为时保持该值不变。

对于采集程序,导出前应验证最终内容类型和必需字段。状态为 200 的登录页不应被保存为成功获取的商品记录。如果重定向只能进入一组已知主机,应显式检查并跟随获准的步骤;数字形式的重定向次数限制并不等于主机白名单。

常见问题

常见问题

curl 会跟随 JavaScript 重定向吗?

不会。--location 跟随的是 HTTP 重定向,不执行 JavaScript,也不提供浏览器运行环境。任务需要执行页面代码时,应选择浏览器工作流程。

可以使用 --max-redirs 0 吗?

启用重定向跟随后,将上限设置为零会阻止跟随任何重定向。如果遇到意外重定向时就应停止请求,这项设置很有用。

为什么最终状态是 200,却没有想要的数据?

请检查最终 URL、响应类型和正文。重定向链可能到达了其他页面。除了传输成功,还需要验证内容。

为请求选择合适的工作流程

HTTP 请求可以参考代理连接示例;如果任务需要的能力超出重定向处理,应比较浏览器和 API 路线。

下一步

运行本地演示

下载 network_demo.py,或将以下完整代码保存为同名文件。使用 Python 3 运行 python network_demo.py。服务仅监听 127.0.0.1:8766,使用完按 Ctrl+C 退出。

"""Local HTTP demonstration. No external requests, credentials or MIYAIP service calls."""
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlsplit
import json
import time


class DemoHandler(BaseHTTPRequestHandler):
    def log_message(self, *_):
        pass

    def reply(self, code, body=b"", headers=None):
        self.send_response(code)
        for key, value in (headers or {}).items():
            self.send_header(key, value)
        self.send_header("Content-Length", str(len(body)))
        try:
            self.end_headers()
            self.wfile.write(body)
        except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
            pass

    def do_CONNECT(self):
        self.reply(407, headers={"Proxy-Authenticate": 'Basic realm="local-demo"'})

    def do_GET(self):
        path = urlsplit(self.path).path
        if path == "/redirect":
            return self.reply(302, headers={"Location": "/headers"})
        if path == "/loop":
            return self.reply(302, headers={"Location": "/loop"})
        if path == "/slow":
            time.sleep(2)
        if path in {"/headers", "/slow"}:
            payload = {"method": self.command, "headers": dict(self.headers)}
            return self.reply(200, json.dumps(payload, indent=2).encode(),
                              {"Content-Type": "application/json"})
        self.reply(404, b"Use /headers, /redirect, /loop or /slow")

    def do_POST(self):
        body = self.rfile.read(int(self.headers.get("Content-Length", "0")))
        if urlsplit(self.path).path == "/redirect307":
            return self.reply(307, headers={"Location": "/headers"})
        if urlsplit(self.path).path == "/redirect302":
            return self.reply(302, headers={"Location": "/headers"})
        payload = {"method": self.command, "headers": dict(self.headers),
                   "body": body.decode("utf-8", errors="replace")}
        self.reply(200, json.dumps(payload, indent=2).encode(),
                   {"Content-Type": "application/json"})


if __name__ == "__main__":
    print("Local demonstration: http://127.0.0.1:8766 (Ctrl+C to stop)", flush=True)
    ThreadingHTTPServer(("127.0.0.1", 8766), DemoHandler).serve_forever()

来源