很多同学用 Python 的 Requests 库写接口测试基本停留在“会调requests.get()”“会带headers”“会解析.json()”的层面。一旦遇到复杂的鉴权、代理切换、连接池复用、重试策略、流式响应或者需要在一个自动化测试平台里把请求底层统一接管时就会卡住文档翻了不少搜索出来的资料都是同一个例子换个皮真正能解答“Requests 内部到底怎么工作的”内容非常少。这篇教程的核心思路是直接“手撕” Requests 底层源码从请求对象如何构造、Session 如何维持状态、HTTPAdapter 如何跟 urllib3 交互到 Response 如何被包装成型完整走一遍。读完之后你对接口测试的理解会从“调库”变成“控库”后面不管是自研测试框架、二次封装、还是排查线上诡异问题都会顺手很多。本文适合以下几类读者已经能用 Requests 写简单接口测试但想深入源码提升功底的测试开发。打算自建 AI 自动化测试平台或接口测试平台需要把 HTTP 请求层统一封装的后端开发者。面试前想系统梳理 Requests 源码知识点准备“手撕源码”类问题的求职者。阅读本文大约需要 60 到 90 分钟建议打开本机 Python 环境一边读一边在源码里跳转印象会深很多。1. Requests 库的定位它到底帮你做了什么先问一个问题不使用 Requests直接用 Python 标准库写 HTTP 请求你会做什么你需要手动处理urllib.request拼接 URL、构造请求头、处理Cookie、处理重定向、报错分类、响应编码……这一串操作做下来代码会变得非常长而且容易在不同 Python 版本间出现兼容性问题。Requests 诞生的初衷就是把这些底层的 HTTP 操作封装成简洁、符合直觉的 API。从源码层面看Requests 本身并不是一个纯粹的 HTTP 协议实现库。它底层依赖另一个著名的库urllib3。两者的关系是urllib3负责真正建立网络连接、管理连接池、发送 HTTP 请求、处理 TCP 层重传等底层工作。requests在urllib3之上做了一层更友好的封装提供了Session、Cookie持久化、auth认证器、hooks钩子、PreparedRequest预请求对象等概念。所以“手撕 Requests 源码”的核心不是让你去看 HTTP 协议怎么编码而是搞清楚 Requests 如何把“用户传入的参数”一步步转换成“urllib3 能识别的请求”再把“urllib3 返回的原始响应”包装成“开发者友好的 Response 对象”。整个模块结构可以简单理解为文件职责requests/api.py顶层 APIrequests.get()、requests.post()等入口requests/sessions.pySession对象核心类负责请求生命周期requests/models.pyRequest、PreparedRequest、Response三大数据对象requests/adapters.pyHTTPAdapter连接urllib3的桥梁requests/auth.py认证器处理Basic Auth、Digest Auth等requests/hooks.py钩子系统允许在请求前后插入自定义逻辑requests/utils.py工具函数URL 拼接、编码判断等requests/cookies.pyCookie持久化逻辑requests/exceptions.py异常体系按错误类型细分接下来我们按一次真实请求的执行链路从入口到返回逐层拆解。2. 环境准备与版本说明开始之前建议先确认本地 Python 环境。本文以常见稳定环境为例# 查看 Python 版本 python --version # 查看 requests 版本 pip show requests # 查看 urllib3 版本 pip show urllib3如果尚未安装 Requests执行pip install requests源码阅读建议直接在 IDE 中跳转也可以直接进入 Requests 安装目录查看源码文件。以常见环境为例源码位置通常在这个路径下# 以 Python 3.8 且安装在 site-packages 为例 python -c import requests; print(requests.__file__)输出结果一般指向site-packages/requests/__init__.py同一目录下就能看到上面表格里列出的所有源码文件。版本差异说明本文源码逻辑基于 Requests 2.x 较新的版本整体架构保持了高度稳定。你本地的版本如果略有不同核心类名、方法名、调用链不会变细节实现可能有微调踩坑时先确认版本即可。如果你对 urllib3 的源码也感兴趣可以顺带查看python -c import urllib3; print(urllib3.__file__)不过本文的重点是 Requests 层urllib3 只会在HTTPAdapter的接线处做说明。3. 从requests.get()看一次请求的全部源码链路在源码阅读里最忌讳一上来就啃整个文件。我建议先看一条最熟悉的调用链import requests resp requests.get(https://httpbin.org/get) print(resp.status_code) print(resp.text)这行代码背后发生了什么打开requests/api.py你会看到类似这样的实现def get(url, paramsNone, **kwargs): return request(get, url, paramsparams, **kwargs) def request(method, url, **kwargs): with sessions.Session() as session: return session.request(methodmethod, urlurl, **kwargs)也就是说即使你直接调用requests.get()Requests 也会临时创建一个Session对象然后调用session.request()完成请求。这也是为什么在接口测试中我们经常强调“使用 Session 而不是裸调 requests.get()”——因为底层的机制本来就是 Session。接着进入requests/sessions.py的Session.request()方法。这个方法做了非常多的事情核心流程可以简化为合并Session级别的默认配置和本次请求的临时配置。构造一个Request对象。调用Session.prepare_request()生成PreparedRequest。调用Session.send()发送请求。返回Response对象。我们来看一下request()方法的核心片段注意这里为便于理解做了精简实际源码还包含大量边界处理def request(self, method, url, paramsNone, dataNone, headersNone, cookiesNone, ...): # 构造 Request req Request( methodmethod.upper(), urlurl, headersheaders, filesfiles, datadata or {}, jsonjson, paramsparams or {}, authauth, cookiescookies, hookshooks, ) prep self.prepare_request(req) # 发送并返回 Response return self.send(prep, streamstream, verifyverify, certcert, proxiesproxies)到这一步你能看到 Requests 中最重要的一个分层思路Request属于“用户意图”用户想请求什么。PreparedRequest属于“准备就绪的请求”已合并配置、已编码参数、已设置好请求头。Response属于“服务器响应”状态码、响应头、响应体、Cookies。接口测试中我们经常需要在请求发出前统一修改参数或者添加签名。理解了这层关系你就知道最佳插入点是在prepare_request()前后或 hooks 里而不是在请求已经发出去之后再来补救。4. 手撕核心Session 是如何保持状态和复用的Session 是 Requests 源码中使用频率最高也是面试中最常被问到的对象。它解决的问题是让多个请求共享同一个状态。什么状态主要有三类Cookie 状态。默认请求头。连接池通过底层适配器与 urllib3 共享。Session在sessions.py中的核心属性如下class Session(SessionRedirectMixin): def __init__(self): self.headers default_headers() self.auth None self.proxies {} self.hooks default_hooks() self.params {} self.stream False self.verify True self.cert None self.max_redirects 30 self.trust_env True self.cookies cookiejar_from_dict({}) self.adapters OrderedDict() # 注册了两个默认适配器HTTP 和 HTTPS self.mount(https://, HTTPAdapter()) self.mount(http://, HTTPAdapter())注意最后两行mount()操作。这里揭示了连接池的挂载方式Requests 针对不同的 URL 前缀schema://host:port级别适配不同的HTTPAdapter。默认情况下HTTP 和 HTTPS 都会被挂到同一个默认适配器上。如果你在接口测试中需要自定义连接池、自定义重试策略或者针对特定域名走不同代理底层改的就是这些Adapter。# 示例为某个特定前缀挂载自定义适配器 import requests from requests.adapters import HTTPAdapter session requests.Session() adapter HTTPAdapter(pool_connections20, pool_maxsize20) session.mount(https://httpbin.org, adapter)session.send()的核心逻辑会根据 URL 找到对应的 adapter然后调用adapter.send()。def send(self, request, **kwargs): # 根据 url 的 scheme 获取 adapter adapter self.get_adapter(urlrequest.url) # 调用 adapter 发送请求 r adapter.send(request, **kwargs) # 构建 Response 并返回 return r从这个设计可以看出扩展 Requests 时除了改 Session更优雅的方式是自定义 Adapter。比如要支持 Unix Socket、自定义 DNS 解析业界标准做法都是继承HTTPAdapter然后覆写相关方法。5. 深入 HTTPAdapterRequests 与 urllib3 的接线处HTTPAdapter是 Requests 中最接近底层的模块代码量不大但信息密度非常高。它最核心的方法是send()。adapter.send()做的事情可以用一张时序图概括这里不用图形用文字描述步骤根据参数构造urllib3的请求参数包括方法、URL、请求体、请求头。从 Session 配置中读取超时、代理、证书验证等参数。检查是否有代理配置分别走代理连接或直连。通过urllib3.PoolManager或ProxyManager发起真正的网络请求。拿到urllib3.HTTPResponse后包装成requests.Response。释放连接处理重定向、重试等上层逻辑。其中最重要的连接管理逻辑在urllib3的链接池里。Requests 默认的连接池相关参数是继承自urllib3.PoolManagerpool_connections缓存连接池的数量。pool_maxsize每个池最多保存的连接数。max_retries最大重试次数。这些参数可以在构造HTTPAdapter时传入import requests from requests.adapters import HTTPAdapter adapter HTTPAdapter( pool_connections10, pool_maxsize100, max_retries3, ) session requests.Session() session.mount(https://, adapter)如果你在做接口测试时发现请求频繁超时、卡顿或者并发一高就报Connection pool is full大概率就是没有调大这两个池参数。深入源码后你会知道pool_maxsize控制的是单个 host 下可复用连接的数量而不是全局连接数。再来看send()中如何处理代理。Requests 在Session.request()时会把代理配置传下来经由adapter.send()选择对应的ProxyManager。生产环境中如果是内网测试环境代理配置写错会导致请求直接被拒或三百秒超时这类问题在源码里定位非常快。6. PreparedRequest所有钩子和拦截逻辑的发挥空间在接口自动化测试平台中最常见的需求之一就是在请求发出前统一加签名、统一加 token、统一打日志。这些逻辑的最佳插入点就是PreparedRequest的构造过程。看sessions.py中的prepare_request()def prepare_request(self, request): # 合并 Cookie merged_cookies merge_cookies( merge_cookies(RequestsCookieJar(), self.cookies), request.cookies, ) # 构造 PreparedRequest p PreparedRequest() p.prepare( methodrequest.method.upper(), urlrequest.url, filesrequest.files, datarequest.data, jsonrequest.json, headersmerge_setting(request.headers, self.headers), paramsmerge_setting(request.params, self.params), authmerge_setting(auth, self.auth), cookiesmerged_cookies, hooksmerge_hooks(request.hooks, self.hooks), ) return pPreparedRequest.prepare()内部会调用一系列prepare_*方法例如prepare_method()prepare_url()prepare_headers()prepare_body()prepare_auth()prepare_cookies()prepare_hooks()这一系列方法的功能是把用户传入的参数变成符合 HTTP 规范的格式。例如prepare_url()内部会处理 URL 编码、默认端口、认证信息提取等逻辑prepare_body()会根据传入的data、json、files自动计算Content-Type并编码请求体。理解这套机制以后接口测试中的很多难题都有了解法。比如你要登录后拿到 token并在后续所有请求中自动带上最简单的方式是覆写 Sessionimport requests class TokenSession(requests.Session): def __init__(self, tokenNone): super().__init__() self.token token def prepare_request(self, request): request.headers request.headers or {} if self.token and Authorization not in request.headers: request.headers[Authorization] fBearer {self.token} return super().prepare_request(request) session TokenSession(tokenyour_token_here) resp session.get(https://httpbin.org/headers) print(resp.json())这段代码的精髓就是在 Session 层覆写prepare_request()在 PreparedRequest 真正生成前把 token 注入。相比在每次调用时手动传headers这种方案可以从源头解决问题让测试代码干净很多。另外hooks 机制也是插桩的好帮手。requests.hooks默认支持两类钩子response响应对象生成后、返回给用户前执行。示例import requests def log_response(response, *args, **kwargs): print(f[HOOK] status: {response.status_code}, url: {response.url}) return response resp requests.get(https://httpbin.org/get, hooks{response: log_response})这段代码会在请求返回后自动打印状态码和 URL。真实测试平台中可以在该钩子里做统一断言、统一记录耗时、统一写入报告。7. 深入 Response拿到响应之后发生了什么响应对象是接口测试中最常打交道的对象。打开requests/models.py定位到Response类。常见的resp.status_code、resp.text、resp.content、resp.json()都在这里定义。其中几个核心细节需要重点理解7.1resp.content与resp.textresp.content是响应体的原始字节类型为bytes。resp.text是根据编码解码后的字符串。源码里text的实现大致是property def text(self): encoding self.encoding or self.apparent_encoding return str(self.content, encoding, errorsreplace)如果响应头里没有charsetRequests 会尝试从响应体推断编码。这也是为什么有些接口返回的 JSON 里中文乱码实际原因往往是服务端响应头未声明编码而 Requests 猜测编码不准确。解决方式是在读取.text前手动指定resp.encoding utf-8 print(resp.text)7.2resp.json()resp.json()内部是基于json.loads(resp.text)实现的所以当接口返回的不是合法 JSON 时会抛出requests.exceptions.JSONDecodeError。这个异常类是 Requests 异常体系里比较新增加的一类做接口测试时要注意捕获。7.3resp.rawresp.raw是 urllib3 的原始响应对象可以理解为“还没有完全被读取的底层响应”。做文件下载、大响应流式处理时需要用到流式请求import requests with requests.get(https://httpbin.org/stream/5, streamTrue) as r: for line in r.iter_lines(): if line: print(line.decode(utf-8))这里的iter_lines()内部就会用到resp.raw的分块读取机制避免一次性把大响应全部加载到内存。8. 实战基于源码理解做一套接口测试底层封装理解了以上源码链路后我们做一个综合实战手写一个精简但适合放进测试平台的 HTTP 客户端封装把 Session 复用、超时管理、自动重试、日志钩子、token 自动注入全部整合在一起。下面先看完整代码然后逐个模块解释。假设项目结构如下api_platform/ ├── http_client.py ├── test_demo.py └── requirements.txtrequirements.txtrequests2.31.0 pytest7.4.0http_client.pyimport logging import time import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry logger logging.getLogger(api_platform.http_client) class ApiClient(requests.Session): 基于 requests.Session 的接口测试客户端封装 def __init__(self, base_url, timeout10, tokenNone): super().__init__() self.base_url base_url.rstrip(/) self.timeout timeout self.token token # 统一设置请求头 self.headers.update({ User-Agent: ApiPlatform/1.0, Accept: application/json, }) # 配置连接池 adapter HTTPAdapter( pool_connections10, pool_maxsize50, max_retriesRetry( total3, backoff_factor1, status_forcelist[500, 502, 503, 504], allowed_methods[GET, POST, PUT, DELETE], ), ) self.mount(https://, adapter) self.mount(http://, adapter) # 注册响应钩子统一记录日志 self.hooks[response].append(self._log_response) def _resolve_url(self, url): if url.startswith(http): return url return f{self.base_url}/{url.lstrip(/)} def _inject_token(self, request): if self.token: request.headers[Authorization] fBearer {self.token} def prepare_request(self, request): # 在 PreparedRequest 生成前统一注入 token if self.token and not request.headers.get(Authorization): self._inject_token(request) return super().prepare_request(request) def _log_response(self, response, *args, **kwargs): cost_ms response.elapsed.total_seconds() * 1000 logger.info( [HTTP] %s %s - %s, cost%.1fms, response.request.method, response.request.url, response.status_code, cost_ms, ) return response def request(self, method, url, **kwargs): # 统一设置超时 kwargs.setdefault(timeout, self.timeout) url self._resolve_url(url) return super().request(method, url, **kwargs) # 为了方便测试模块直接导入创建默认实例 default_client ApiClient()代码解释继承requests.Session而不是简单包一层函数这样所有请求自动共享 Cookie、连接池、默认请求头。构造方法里配置了Retry使用 urllib3 的Retry类替代简单的max_retries数字可以精细控制哪些状态码需要重试、退避策略如何计算。prepare_request()覆写点完成 token 注入后续测试代码完全不感知鉴权逻辑。request()方法统一拼 URL、统一加超时减少重复代码。_log_response钩子统一打印请求方法、URL、状态码、耗时方便测试报告采集。接下来写一个简单测试用例验证封装的可用性test_demo.pyimport pytest from http_client import default_client def test_get_json(): resp default_client.get(https://httpbin.org/get) assert resp.status_code 200 data resp.json() assert url in data def test_post_json(): resp default_client.post( https://httpbin.org/post, json{name: tester, level: senior}, ) assert resp.status_code 200 data resp.json() assert data[json][name] tester def test_invalid_json_error(): resp default_client.get(https://httpbin.org/html) with pytest.raises(Exception): _ resp.json()执行测试python -m pytest test_demo.py -v运行后正常会输出三组用例结果并且每条请求都会打印一行日志内容类似[HTTP] GET https://httpbin.org/get - 200, cost123.4ms [HTTP] POST https://httpbin.org/post - 200, cost150.2ms [HTTP] GET https://httpbin.org/html - 200, cost80.9ms到这里一个基础但可扩展到测试平台的 HTTP 客户端就完成了。基于源码理解你能很清楚地说出每个覆写点的作用prepare_request()用于请求发出前拦截request()用于参数兜底hooks[response]用于统一处理响应。如果后续要接入测试平台只需在此封装基础上新增请求日志落库。接口响应断言。失败用例截图或报文快照保存。结合 AI 接口自动生成测试数据。这些动作都是在上述三个入口上做扩展而不是去改测试用例。9. 常见问题与排查思路在深入源码之后很多接口测试中的高频问题可以快速定位到根因。我整理了几个非常典型的排查案例。问题现象常见原因解决思路请求报Connection pool is full连接池数量不足高并发时排队等待调大HTTPAdapter.pool_maxsize必要时调大pool_connections响应中文乱码服务端响应头未声明charsetRequests 编码推测错误手动设置resp.encoding utf-8后再读.text返回 500 后重试仍然失败重试策略未正确设置或方法不在允许重试范围内使用urllib3.Retry配置status_forcelist和allowed_methods某些接口总是走代理导致超时环境变量HTTP_PROXY、HTTPS_PROXY被继承在Session.request()中显式传proxies{}或设置trust_envFalse响应 302 后无法拿到真实 URL自动重定向默认开启但部分场景需要手动控制设置allow_redirectsFalse然后从resp.headers[Location]提取跳转地址token 过期后所有请求开始 401之前手动在每个请求里带 token未统一管理覆写prepare_request()集中注入必要时增加自动刷新逻辑文件下载时内存突然暴涨默认streamFalse一次性读取全文使用streamTrue配合iter_content()分块读取JSON 解析报错响应体不是合法 JSON如 HTML 错误页先看resp.text前 200 个字符或用resp.content定位响应体做接口自动化测试时推荐所有异常场景都先抓原始响应报文。Requests 的异常体系会保留完整的请求信息和响应信息例如import requests try: resp requests.get(https://httpbin.org/status/500) resp.raise_for_status() except requests.HTTPError as e: print(请求失败状态码, e.response.status_code) print(响应内容, e.response.text)raise_for_status()的作用是当状态码在 400 到 600 之间时抛出HTTPError异常。这样处理比每个用例里手动assert状态码要更集中、更可控。10. 最佳实践与工程建议源码读完之后除了能应付面试更重要的是把这些认知落到工程里。下面是我在接口测试平台落地过程中总结出来的一些核心建议。10.1 始终坚持复用 Session很多测试脚本喜欢每个用例requests.get()一把梭。这种方式看起来简洁但如果测试规模上了一百条用例就会暴露出连接频繁建立、Cookie 无法共享、代理配置散落各处的问题。正确的做法是在测试夹具中创建全局 Session按模块复用。import pytest from http_client import default_client pytest.fixture(scopesession) def client(): return default_client10.2 重试要设置“幂等保护”接口测试中不是所有请求都适合自动重试。比如一个订单提交接口如果请求其实已经到达服务端但响应在返回时超时自动重试就可能造成重复下单。因此重试策略要对接口的幂等性做分级查询类接口GET、HEAD可以放心重试。写入类接口POST、PUT、PATCH、DELETE要根据业务设计判断是否允许重复执行。非幂等接口建议关闭自动重试或者改为人工确认。对应到Retry配置就是控制allowed_methods。10.3 超时必须显式设置Requests 默认没有超时这意味着如果服务端不返回数据请求可能一直挂起测试用例会像“死机”一样卡住。在封装时强制设置默认超时非常有必要。推荐在框架层为不同场景定义超时级别DEFAULT_TIMEOUT 10 SLOW_TIMEOUT 30 UPLOAD_TIMEOUT 120然后在请求方法中按场景传入。10.4 日志与链路追踪源码里的hooks机制很适合做日志埋点。推荐在请求钩子中记录以下信息请求方法、URL、请求头关键字段注意脱敏。请求体摘要过大时截断或只记录大小。响应状态码、耗时、响应体摘要。异常时的完整 traceback。如果测试平台接入了统一日志系统可以直接通过这些字段做接口健康度分析。10.5 版本管理与依赖锁定Requests 每个版本的底层实现细节会有差异接口测试框架的依赖必须锁定版本。使用requirements.txt时建议精确到小版本而不是只写requests。requests2.31.0 urllib32.0.4 pytest7.4.010.6 安全注意事项虽然 Requests 默认验证 HTTPS 证书但很多测试环境使用的自签名证书会导致验证失败常见简化做法是verifyFalse。这里必须特别提醒这个配置只应在隔离的测试环境中使用。如果测试环境涉及真实敏感数据禁止全局关闭证书验证正确做法是使用自定义 CA 文件resp requests.get(https://internal.example.com, verify/path/to/ca.pem)如果确实需要对某个内网域名关闭验证请在代码里明确限定域名范围不要写成一个全局参数。11. 总结与下一步学习方向这篇文章沿着一次真实请求的完整链路把 Requests 源码的核心部分拆了一遍requests/api.py只是入口实际工作在Session中完成。Session负责状态管理与连接池挂载是接口测试封装的最佳基座。PreparedRequest是请求发出前最后的拦截点适合做 token 注入、统一签名、参数处理。HTTPAdapter是连接 urllib3 的桥梁连接池、重试、代理都发生在这一层。Response对象的属性背后包含编码、流式处理、异常转换等大量细节。掌握这些点之后你不再只是“会用 Requests”而是能判断 Requests 在什么场景下会有什么表现能快速定位问题也能根据自己的需求去扩展它。下一步可以根据自身方向选择深入学习如果你在做接口测试平台可以继续读urllib3的连接池源码深入了解PoolManager和ConnectionPool的关系。如果你在做 AI 自动化测试可以尝试把 Requests 的钩子与 LLM 接口结合起来实现基于自然语言的 API 测试用例生成。如果你在准备面试可以把本文的调用链整理成一份时序图结合源码标注关键方法做到“讲任何一层都能定位文件位置”。动手读源码最大的好处是用一次深入换未来很多次省事。建议你打开本地 Requests 源码按照本文的路径走一遍。你也可以尝试抛掉本文自己再追踪一遍requests.post()的调用链看看能否独立讲清楚 Session、Adapter、PreparedRequest 三者之间的关系。能讲清楚说明你是真的吃透了。