Requests 重定向与历史

重定向 是Web开发中常见的机制,允许服务器将客户端从一个URL导向到另一个URL。Requests库提供了强大的重定向处理功能,包括自动重定向、历史追踪和精细控制。

什么是重定向?

HTTP重定向是一种服务器响应,告诉客户端请求的资源已移动到另一个位置。服务器通过返回一个特定的状态码(如301, 302, 303, 307, 308)和一个Location头来实现重定向。

客户端请求
GET /old-url
服务器响应
302 Found
Location: /new-url
客户端重定向
GET /new-url
最终响应
200 OK
1 客户端发送请求

浏览器或客户端应用程序向服务器发送HTTP请求

GET /old-page HTTP/1.1
Host: example.com
2 服务器返回重定向

服务器返回3xx状态码和Location头部,指示新的URL

HTTP/1.1 302 Found
Location: https://example.com/new-page
3 客户端跟随重定向

客户端自动向新URL发送请求(如果允许自动重定向)

GET /new-page HTTP/1.1
Host: example.com
4 服务器返回最终响应

服务器返回请求的资源或另一个重定向

HTTP/1.1 200 OK
Content-Type: text/html

<html>...</html>

HTTP重定向类型

状态码 名称 描述 Requests处理方式
301 Moved Permanently 永久重定向。资源已永久移动到新位置,客户端应更新书签。 默认自动跟随,HTTP方法可能从POST改为GET
302 Found 临时重定向。资源暂时移动到新位置。 默认自动跟随,HTTP方法可能从POST改为GET
303 See Other 参见其他。通常用于POST后的重定向,客户端应使用GET请求新URL。 默认自动跟随,HTTP方法改为GET
307 Temporary Redirect 临时重定向。与302类似,但HTTP方法保持不变。 默认自动跟随,HTTP方法保持不变
308 Permanent Redirect 永久重定向。与301类似,但HTTP方法保持不变。 默认自动跟随,HTTP方法保持不变
永久重定向 (301, 308)
  • 资源永久移动到新位置
  • 搜索引擎会更新索引
  • 客户端应更新书签
  • 缓存可能存储重定向
临时重定向 (302, 303, 307)
  • 资源暂时移动到新位置
  • 搜索引擎不会更新索引
  • 客户端不应更新书签
  • 缓存通常不存储重定向

自动重定向

默认情况下,Requests会自动处理重定向。当服务器返回3xx状态码时,Requests会自动向Location头部指定的新URL发送请求。

默认的自动重定向行为
import requests

# 默认情况下,Requests会自动跟随重定向
response = requests.get('http://httpbin.org/redirect/2')

# 即使原始请求返回302,最终响应是200
print(f"最终状态码: {response.status_code}")  # 200
print(f"请求URL: {response.url}")           # 最终URL
print(f"历史记录: {response.history}")      # 重定向历史

# 查看重定向链中的每个响应
for resp in response.history:
    print(f"重定向 {resp.status_code}: {resp.url}")
[请求] GET http://httpbin.org/redirect/2
[重定向] 302 Found → Location: /redirect/1
[重定向] 302 Found → Location: /get
[响应] 200 OK (最终响应)
自动重定向的默认行为
  • Requests默认自动跟随最多30次重定向
  • 对于301、302、303重定向,POST请求会变为GET请求(符合HTTP规范)
  • 对于307、308重定向,HTTP方法保持不变
  • 重定向历史存储在response.history列表中
  • 最终响应存储在response对象中

重定向历史

Requests会记录重定向过程中的所有中间响应,可以通过response.history访问。

重定向历史时间线
302
初始请求
GET https://example.com/old-path
301
第一次重定向
GET https://example.com/intermediate
302
第二次重定向
GET https://www.example.com/new-path
200
最终响应
GET https://www.example.com/new-path
分析重定向历史
import requests

def analyze_redirects(url):
    """分析URL的重定向链"""

    response = requests.get(url, allow_redirects=True)

    print(f"分析: {url}")
    print(f"最终URL: {response.url}")
    print(f"最终状态码: {response.status_code}")
    print(f"重定向次数: {len(response.history)}")
    print()

    if response.history:
        print("重定向历史:")
        print("-" * 80)

        for i, resp in enumerate(response.history, 1):
            print(f"{i}. 状态码: {resp.status_code}")
            print(f"   请求URL: {resp.url}")
            print(f"   重定向到: {resp.headers.get('Location', '未知')}")
            print(f"   响应头: {dict(resp.headers)}")
            print("-" * 80)

    return response

# 使用示例
if __name__ == "__main__":
    # 测试重定向链
    result = analyze_redirects('http://httpbin.org/redirect/3')

    # 获取最终响应的详细信息
    print(f"\n最终响应:")
    print(f"URL: {result.url}")
    print(f"内容类型: {result.headers.get('content-type')}")
    print(f"内容长度: {len(result.content)} 字节")

历史响应对象

response.history是一个列表,包含所有重定向的响应对象。每个历史响应对象都包含:

  • status_code: 重定向状态码 (301, 302, 等)
  • url: 该次请求的URL
  • headers: 响应头部,包含Location
  • request: 对应的请求对象
  • elapsed: 请求耗时
访问历史响应的详细信息
import requests

# 发送请求并跟随重定向
response = requests.get('https://httpbin.org/redirect/2')

# 检查是否有重定向历史
if response.history:
    print("发现重定向历史:")

    # 遍历所有历史响应
    for i, historical_response in enumerate(response.history, 1):
        print(f"\n第 {i} 次重定向:")
        print(f"  状态码: {historical_response.status_code}")
        print(f"  请求URL: {historical_response.url}")
        print(f"  请求方法: {historical_response.request.method}")
        print(f"  响应头: {dict(historical_response.headers)}")
        print(f"  重定向目标: {historical_response.headers.get('Location')}")
        print(f"  请求耗时: {historical_response.elapsed}")

        # 也可以访问请求对象
        request = historical_response.request
        print(f"  原始请求头: {dict(request.headers)}")
        print(f"  原始请求体: {request.body}")
else:
    print("没有重定向历史")

print(f"\n最终响应:")
print(f"URL: {response.url}")
print(f"状态码: {response.status_code}")

控制重定向行为

Requests提供了多种选项来控制重定向行为,包括禁用自动重定向、限制重定向次数等。

重定向控制选项
allow_redirects
是否允许自动重定向 (默认: True)

设置为False时,Requests不会自动跟随重定向,返回第一个3xx响应。

max_redirects
最大重定向次数 (默认: 30)

超过此限制会引发TooManyRedirects异常。

hooks
请求钩子,用于监控重定向

可以在重定向发生时执行自定义逻辑。

timeout
超时设置,包括重定向时间

总超时时间包括所有重定向的时间。

禁用自动重定向

禁用自动重定向
import requests

# 禁用自动重定向
response = requests.get('http://httpbin.org/redirect/2', allow_redirects=False)

print(f"状态码: {response.status_code}")  # 302 (不是200)
print(f"响应URL: {response.url}")       # 原始URL,不是最终URL
print(f"Location头: {response.headers.get('Location')}")
print(f"历史记录: {response.history}")  # 空列表

# 手动处理重定向
if response.status_code in [301, 302, 303, 307, 308]:
    redirect_url = response.headers['Location']
    print(f"需要重定向到: {redirect_url}")

    # 手动发送新的请求
    new_response = requests.get(redirect_url)
    print(f"最终状态码: {new_response.status_code}")
    print(f"最终URL: {new_response.url}")

限制最大重定向次数

设置最大重定向次数
import requests
from requests.exceptions import TooManyRedirects

# 设置最大重定向次数
try:
    # 尝试访问一个有很多重定向的URL
    response = requests.get('http://httpbin.org/redirect/5', max_redirects=3)
except TooManyRedirects as e:
    print(f"重定向次数过多: {e}")
    print(f"允许的最大重定向次数: 3")

    # 可以获取到最后一个响应
    # 注意:在异常处理中,可以通过e.response访问最后一个响应
    if hasattr(e, 'response') and e.response:
        print(f"最后一个响应的状态码: {e.response.status_code}")
        print(f"最后一个响应的URL: {e.response.url}")
        print(f"已经发生的重定向次数: {len(e.response.history)}")

# 或者通过Session设置默认的最大重定向次数
session = requests.Session()
session.max_redirects = 5  # 设置Session级别的最大重定向次数

try:
    response = session.get('http://httpbin.org/redirect/10')
except TooManyRedirects:
    print("Session级别的重定向限制生效")

使用钩子监控重定向

使用钩子监控重定向过程
import requests

def redirect_hook(response, **kwargs):
    """重定向钩子函数"""
    print(f"发生重定向:")
    print(f"  从: {response.url}")
    print(f"  状态码: {response.status_code}")
    print(f"  重定向到: {response.headers.get('Location')}")
    print(f"  重定向历史长度: {len(response.history)}")

    # 可以在这里添加自定义逻辑
    # 例如:记录日志、修改请求、检查重定向目标等

    return response

# 创建自定义钩子
hooks = {'response': [redirect_hook]}

# 发送请求并监控重定向
response = requests.get(
    'http://httpbin.org/redirect/3',
    hooks=hooks,
    allow_redirects=True
)

print(f"\n最终结果:")
print(f"最终URL: {response.url}")
print(f"最终状态码: {response.status_code}")
print(f"总重定向次数: {len(response.history)}")

# 也可以在Session级别设置钩子
session = requests.Session()
session.hooks['response'].append(redirect_hook)

# Session的所有请求都会调用钩子
session.get('http://httpbin.org/redirect/2')

手动处理重定向

在某些情况下,你可能需要手动处理重定向,例如:

  • 需要控制重定向逻辑
  • 需要修改重定向请求
  • 需要处理特殊的重定向场景
  • 需要避免无限重定向循环
手动处理重定向的完整示例
import requests
from urllib.parse import urljoin, urlparse

class ManualRedirectHandler:
    """手动重定向处理器"""

    def __init__(self, max_redirects=10, timeout=30):
        self.max_redirects = max_redirects
        self.timeout = timeout
        self.session = requests.Session()
        self.redirect_history = []

    def resolve_url(self, url, original_method='GET', data=None, headers=None):
        """手动解析URL重定向链"""

        current_url = url
        current_method = original_method
        current_data = data
        redirect_count = 0

        while redirect_count < self.max_redirects:
            # 发送请求(不自动重定向)
            response = self.session.request(
                method=current_method,
                url=current_url,
                data=current_data,
                headers=headers,
                allow_redirects=False,
                timeout=self.timeout
            )

            # 记录历史
            self.redirect_history.append({
                'url': current_url,
                'method': current_method,
                'status_code': response.status_code,
                'location': response.headers.get('Location')
            })

            # 检查是否是重定向
            if response.status_code in [301, 302, 303, 307, 308]:
                redirect_count += 1

                # 获取重定向目标
                location = response.headers.get('Location')
                if not location:
                    raise ValueError(f"重定向响应缺少Location头: {response.status_code}")

                # 解析相对URL
                if not urlparse(location).netloc:
                    location = urljoin(current_url, location)

                # 根据重定向类型调整HTTP方法
                if response.status_code in [301, 302, 303]:
                    # 对于这些状态码,POST应该变为GET
                    if current_method == 'POST':
                        current_method = 'GET'
                        current_data = None  # GET请求没有请求体
                # 307和308保持HTTP方法不变

                print(f"重定向 {redirect_count}: {response.status_code} {current_url} → {location}")
                current_url = location

            else:
                # 不是重定向,返回最终响应
                print(f"解析完成,共 {redirect_count} 次重定向")
                return response

        # 超过最大重定向次数
        raise requests.exceptions.TooManyRedirects(
            f"超过最大重定向次数: {self.max_redirects}"
        )

    def get_redirect_chain(self):
        """获取重定向链"""
        return self.redirect_history

# 使用示例
if __name__ == "__main__":
    handler = ManualRedirectHandler(max_redirects=5)

    try:
        # 手动解析重定向
        response = handler.resolve_url(
            'http://httpbin.org/redirect/3',
            original_method='GET'
        )

        print(f"\n最终响应:")
        print(f"URL: {response.url}")
        print(f"状态码: {response.status_code}")

        print(f"\n重定向链:")
        for i, entry in enumerate(handler.get_redirect_chain(), 1):
            print(f"{i}. {entry['method']} {entry['url']} → {entry['status_code']} → {entry['location']}")

    except requests.exceptions.TooManyRedirects as e:
        print(f"重定向次数过多: {e}")
    except Exception as e:
        print(f"错误: {e}")
手动重定向的常见场景
场景1: 修改重定向请求

需要在重定向时添加或修改请求头、参数等。

# 在重定向时添加自定义头
if response.status_code == 302:
    redirect_url = response.headers['Location']
    headers = {'X-Custom-Header': 'value'}
    response = requests.get(redirect_url, headers=headers)
场景2: 处理相对URL

需要正确处理相对URL重定向。

from urllib.parse import urljoin

# 解析相对URL
base_url = 'https://example.com/path/'
relative_url = '../new-path'
absolute_url = urljoin(base_url, relative_url)
# 结果: https://example.com/new-path
场景3: 检测重定向循环

防止无限重定向循环。

visited_urls = set()
current_url = start_url

while current_url not in visited_urls:
    visited_urls.add(current_url)
    # 发送请求并检查重定向
    # ...
场景4: 选择性跟随重定向

只跟随特定域名的重定向。

from urllib.parse import urlparse

parsed = urlparse(redirect_url)
if parsed.netloc == 'allowed-domain.com':
    # 跟随重定向
    pass
else:
    # 不跟随重定向
    pass

高级主题

POST请求的重定向行为

POST请求的重定向行为比GET请求更复杂,不同状态码有不同的处理方式。

状态码 HTTP方法变化 请求体处理 规范依据
301, 302 POST → GET 请求体被丢弃 历史原因,不符合RFC
303 POST → GET 请求体被丢弃 符合RFC 7231
307, 308 POST → POST 请求体被保留 符合RFC 7231
处理POST请求重定向
import requests

# 测试不同状态码的POST重定向行为
test_cases = [
    ('http://httpbin.org/status/301', '301'),
    ('http://httpbin.org/status/302', '302'),
    ('http://httpbin.org/status/303', '303'),
    ('http://httpbin.org/status/307', '307'),
]

for url, status in test_cases:
    print(f"\n测试 {status} 重定向:")

    try:
        # 发送POST请求
        response = requests.post(
            url,
            data={'key': 'value', 'test': 'data'},
            allow_redirects=True
        )

        # 检查历史记录
        if response.history:
            for hist in response.history:
                print(f"  重定向: {hist.status_code}, 方法: {hist.request.method}")

        print(f"  最终方法: {response.request.method}")

    except Exception as e:
        print(f"  错误: {e}")

# 手动处理POST重定向以保留数据
def post_with_manual_redirect(url, data):
    """手动处理POST重定向以保留请求体"""

    session = requests.Session()
    response = session.post(url, data=data, allow_redirects=False)

    while response.status_code in [301, 302, 303, 307, 308]:
        redirect_url = response.headers['Location']

        # 对于307/308,保持POST方法和请求体
        if response.status_code in [307, 308]:
            print(f"保持POST方法重定向到: {redirect_url}")
            response = session.post(redirect_url, data=data, allow_redirects=False)
        else:
            # 对于301/302/303,变为GET方法
            print(f"变为GET方法重定向到: {redirect_url}")
            response = session.get(redirect_url, allow_redirects=False)

    return response

重定向与安全

重定向安全注意事项
  • 开放重定向漏洞: 不要信任用户提供的重定向URL,防止钓鱼攻击
  • 相对URL解析: 注意相对URL可能指向非预期的位置
  • 协议降级: 避免从HTTPS重定向到HTTP,防止中间人攻击
  • 域名验证: 检查重定向目标是否在允许的域名列表中
  • 大小限制: 限制重定向链长度,防止DoS攻击
安全重定向验证
import requests
from urllib.parse import urlparse, urljoin
from typing import List

class SecureRedirectHandler:
    """安全重定向处理器"""

    def __init__(self, allowed_domains: List[str] = None,
                 allow_cross_protocol: bool = False,
                 max_redirects: int = 10):

        self.allowed_domains = allowed_domains or []
        self.allow_cross_protocol = allow_cross_protocol
        self.max_redirects = max_redirects
        self.session = requests.Session()

    def is_safe_redirect(self, current_url: str, redirect_url: str) -> bool:
        """检查重定向是否安全"""

        current = urlparse(current_url)
        redirect = urlparse(redirect_url)

        # 1. 检查协议降级 (HTTPS → HTTP)
        if current.scheme == 'https' and redirect.scheme == 'http':
            if not self.allow_cross_protocol:
                print(f"安全警告: 协议降级 {current.scheme} → {redirect.scheme}")
                return False

        # 2. 检查域名白名单
        if self.allowed_domains:
            if redirect.netloc not in self.allowed_domains:
                print(f"安全警告: 重定向到未允许的域名 {redirect.netloc}")
                return False

        # 3. 检查相对URL解析
        if not redirect.netloc and redirect.path.startswith('//'):
            print(f"安全警告: 可能的协议相对URL {redirect_url}")
            return False

        return True

    def safe_get(self, url: str, **kwargs) -> requests.Response:
        """安全地发送GET请求并处理重定向"""

        current_url = url
        redirect_count = 0

        while redirect_count < self.max_redirects:
            # 发送请求(不自动重定向)
            response = self.session.get(
                current_url,
                allow_redirects=False,
                **kwargs
            )

            # 检查是否是重定向
            if response.status_code in [301, 302, 303, 307, 308]:
                redirect_count += 1

                # 获取重定向目标
                location = response.headers.get('Location')
                if not location:
                    raise ValueError("重定向响应缺少Location头")

                # 解析URL
                if not urlparse(location).netloc:
                    location = urljoin(current_url, location)

                # 安全检查
                if not self.is_safe_redirect(current_url, location):
                    raise ValueError(f"不安全的重定向: {current_url} → {location}")

                print(f"安全重定向 {redirect_count}: {location}")
                current_url = location

            else:
                # 不是重定向,返回最终响应
                return response

        raise requests.exceptions.TooManyRedirects(
            f"超过最大重定向次数: {self.max_redirects}"
        )

# 使用示例
if __name__ == "__main__":
    # 创建安全重定向处理器
    handler = SecureRedirectHandler(
        allowed_domains=['httpbin.org', 'example.com'],
        allow_cross_protocol=False,
        max_redirects=5
    )

    try:
        response = handler.safe_get('https://httpbin.org/redirect/2')
        print(f"安全获取成功: {response.status_code}")
    except ValueError as e:
        print(f"安全错误: {e}")
    except requests.exceptions.TooManyRedirects as e:
        print(f"重定向次数过多: {e}")

重定向处理最佳实践

始终设置最大重定向限制

防止无限重定向循环和DoS攻击

检查重定向历史

了解请求经过了哪些URL

验证重定向目标

防止开放重定向漏洞

处理POST重定向

注意POST请求在重定向时可能变为GET

重定向处理最佳实践示例
import requests
from requests.exceptions import TooManyRedirects, RequestException
from urllib.parse import urlparse

class BestPracticeRedirectHandler:
    """遵循最佳实践的重定向处理器"""

    @staticmethod
    def safe_request(url, method='GET', data=None, **kwargs):
        """
        安全的请求函数,包含重定向最佳实践

        参数:
            url: 请求URL
            method: HTTP方法
            data: 请求数据
            **kwargs: 其他requests参数

        返回:
            Response对象或None(如果失败)
        """

        # 最佳实践配置
        config = {
            'allow_redirects': True,      # 启用自动重定向
            'max_redirects': 10,          # 合理的重定向限制
            'timeout': (3.05, 30),        # 连接和读取超时
            'verify': True,               # 启用SSL验证
        }

        # 合并用户配置
        config.update(kwargs)

        try:
            # 发送请求
            response = requests.request(
                method=method,
                url=url,
                data=data,
                **config
            )

            # 检查重定向历史
            if response.history:
                # 记录重定向信息
                print(f"请求经过了 {len(response.history)} 次重定向")

                # 检查是否有不安全的协议降级
                original_url = urlparse(url)
                final_url = urlparse(response.url)

                if original_url.scheme == 'https' and final_url.scheme == 'http':
                    print("警告: 发生了HTTPS到HTTP的协议降级")

            # 验证最终URL
            final_domain = urlparse(response.url).netloc
            expected_domains = ['example.com', 'api.example.com']  # 你的允许域名

            if final_domain not in expected_domains:
                print(f"警告: 最终域名 {final_domain} 不在允许列表中")

            return response

        except TooManyRedirects as e:
            print(f"错误: 重定向次数过多 ({config['max_redirects']} 次)")

            # 可以获取最后一个响应进行分析
            if hasattr(e, 'response') and e.response:
                print(f"最后一个响应URL: {e.response.url}")
                print(f"重定向历史: {len(e.response.history)} 次")

            return None

        except RequestException as e:
            print(f"请求错误: {e}")
            return None

    @staticmethod
    def analyze_redirect_chain(url):
        """分析URL的重定向链并返回报告"""

        try:
            response = requests.get(url, allow_redirects=True)

            report = {
                'original_url': url,
                'final_url': response.url,
                'final_status_code': response.status_code,
                'redirect_count': len(response.history),
                'redirect_chain': [],
                'security_issues': []
            }

            # 分析重定向链
            previous_url = url
            for i, resp in enumerate(response.history, 1):
                redirect_info = {
                    'step': i,
                    'from': previous_url,
                    'to': resp.headers.get('Location', '未知'),
                    'status_code': resp.status_code,
                    'method_change': None
                }

                # 检查方法变化
                if i > 1:
                    prev_resp = response.history[i-2]
                    if prev_resp.request.method != resp.request.method:
                        redirect_info['method_change'] = {
                            'from': prev_resp.request.method,
                            'to': resp.request.method
                        }

                report['redirect_chain'].append(redirect_info)
                previous_url = resp.headers.get('Location', '未知')

            # 检查安全问题
            for i in range(len(report['redirect_chain'])):
                current = urlparse(report['redirect_chain'][i]['from'])
                next_url = report['redirect_chain'][i]['to']

                if next_url != '未知':
                    nxt = urlparse(next_url)

                    # 检查协议降级
                    if current.scheme == 'https' and nxt.scheme == 'http':
                        report['security_issues'].append(
                            f"步骤 {i+1}: 协议降级 HTTPS → HTTP"
                        )

                    # 检查域名变化
                    if current.netloc != nxt.netloc:
                        report['security_issues'].append(
                            f"步骤 {i+1}: 域名变化 {current.netloc} → {nxt.netloc}"
                        )

            return report

        except Exception as e:
            return {'error': str(e)}

# 使用示例
if __name__ == "__main__":
    # 使用最佳实践函数
    response = BestPracticeRedirectHandler.safe_request(
        'https://httpbin.org/redirect/2',
        method='GET'
    )

    if response:
        print(f"请求成功: {response.status_code}")
        print(f"最终URL: {response.url}")

    # 分析重定向链
    print("\n分析重定向链:")
    report = BestPracticeRedirectHandler.analyze_redirect_chain(
        'https://httpbin.org/redirect/3'
    )

    if 'error' not in report:
        print(f"原始URL: {report['original_url']}")
        print(f"最终URL: {report['final_url']}")
        print(f"重定向次数: {report['redirect_count']}")

        if report['security_issues']:
            print("\n安全问题:")
            for issue in report['security_issues']:
                print(f"  - {issue}")
重定向处理要点总结
  • 理解默认行为: Requests默认自动跟随最多30次重定向
  • 检查历史: 使用response.history了解重定向过程
  • 控制行为: 使用allow_redirectsmax_redirects控制重定向
  • 注意POST重定向: POST请求在301/302/303重定向时会变为GET
  • 验证安全性: 检查重定向目标,防止开放重定向漏洞
  • 处理异常: 捕获TooManyRedirects异常
  • 监控性能: 重定向会增加请求时间,合理设置超时

总结

Requests库提供了强大而灵活的重定向处理功能。关键要点:

  • 自动重定向: Requests默认自动跟随重定向,简化了开发工作
  • 历史追踪: response.history提供了完整的重定向历史记录
  • 精细控制: 可以通过参数控制重定向行为,包括禁用重定向、限制次数等
  • 安全考虑: 需要注意重定向可能带来的安全问题,如开放重定向漏洞
  • POST处理: 不同重定向状态码对POST请求的处理方式不同

合理利用Requests的重定向功能,可以构建健壮、安全的HTTP客户端应用。理解重定向的机制和行为,有助于调试网络问题和优化应用性能。