DevTools 网络分析器与 Dart HttpClient 请求的深度透视:实现细节与拦截策略
I. 引言:网络通信在现代应用中的核心地位与调试挑战
在当今高度互联的软件世界中,几乎所有有意义的应用程序都离不开网络通信。无论是移动应用与后端API的数据交换,桌面应用与云服务的同步,还是Web应用间的协同工作,网络都扮演着数据流动的关键通道。对于使用 Dart 语言,特别是 Flutter 框架构建的应用程序而言,高效、稳定且可调试的网络层是其健康运行的基石。
然而,网络通信的复杂性也带来了巨大的挑战。请求的成功与失败、延迟、数据格式、认证机制、错误处理——这些都可能在开发和维护过程中引发难以诊断的问题。想象一下,一个应用程序在生产环境中偶尔出现数据加载失败,但本地测试却一切正常。此时,我们迫切需要一种工具,能够像“黑匣子”飞行记录器一样,详尽地记录下每一次网络请求的细节,帮助我们洞察问题根源。
DevTools,作为 Flutter 和 Dart 生态系统中的官方调试和性能分析工具套件,正是为了解决此类挑战而生。其中,网络分析器(Network Profiler)是其核心组件之一,它为开发者提供了一个强大的透视镜,用以观察应用程序中所有的网络活动。但仅仅观察是不够的,在许多高级场景下,我们需要在应用程序代码层面“拦截”网络请求,对其进行修改、增强或特殊处理。这就引出了我们今天深入探讨的主题:DevTools 网络分析器如何看待 Dart HttpClient 请求,以及我们如何在 Dart 应用中实现细粒度的请求拦截,同时确保 DevTools 能够正确地反映这些操作。
本讲座将带领大家从 Dart HttpClient 的基础出发,逐步深入到 DevTools 网络分析器的工作原理,最终聚焦于如何在 Dart 代码中构建健壮的请求拦截机制,并理解 DevTools 在这一过程中的角色和局限。我们将通过大量的代码示例,详细阐述各种拦截策略的实现细节,旨在为各位提供一套完整的理论与实践指南。
II. 理解 Dart HttpClient:网络请求的基础
在深入探讨拦截机制之前,我们必须对 Dart 语言中进行网络请求的核心组件有一个清晰的认识。Dart 提供了不同层次的API来处理网络请求,其中最底层、最强大的是 dart:io 库中的 HttpClient。
A. dart:io 中的 HttpClient
HttpClient 是 Dart 平台进行 HTTP 网络通信的基石。它是一个相对低级的 API,直接暴露了 HTTP 协议的细节,允许开发者对请求和响应的各个方面进行精细控制。
1. 核心概念:HttpClient, HttpClientRequest, HttpClientResponse
-
HttpClient: 这是发起 HTTP 请求的客户端对象。它负责管理连接池、代理设置、证书验证等底层网络配置。通常,我们只需要创建一个HttpClient实例,然后通过它来创建HttpClientRequest。import 'dart:io'; void main() async { final client = HttpClient(); // 创建 HttpClient 实例 try { final request = await client.getUrl(Uri.parse('https://api.example.com/data')); // ... 继续处理请求 } finally { client.close(); // 关闭客户端,释放资源 } } -
HttpClientRequest: 代表一个待发送的 HTTP 请求。通过HttpClient的方法(如getUrl,post,put等)创建。你可以设置请求头 (headers)、写入请求体 (add,write,writeString),并最终通过close()方法发送请求。import 'dart:io'; import 'dart:convert'; Future<void> makePostRequest() async { final client = HttpClient(); try { final request = await client.postUrl(Uri.parse('https://api.example.com/submit')); // 设置请求头 request.headers.contentType = ContentType.json; request.headers.add('Authorization', 'Bearer your_token'); // 写入请求体 final Map<String, dynamic> data = {'name': 'Alice', 'age': 30}; request.write(jsonEncode(data)); // 发送请求并等待响应 final response = await request.close(); print('Status: ${response.statusCode}'); response.transform(utf8.decoder).listen(print); } catch (e) { print('Error: $e'); } finally { client.close(); } } -
HttpClientResponse: 代表一个已接收的 HTTP 响应。在HttpClientRequest调用close()并成功发送请求后返回。你可以从HttpClientResponse中读取状态码 (statusCode)、响应头 (headers)、以及响应体 (listen,transform)。它是一个Stream<List<int>>,意味着响应体可以被流式处理。
2. 生命周期:连接、请求、响应、关闭
一个典型的 HttpClient 请求生命周期如下:
- 创建客户端:
HttpClient client = HttpClient(); - 创建请求:
HttpClientRequest request = await client.getUrl(uri); - 配置请求: 设置请求头、请求体。
- 发送请求:
HttpClientResponse response = await request.close();(这会建立连接、发送请求头和体) - 处理响应: 读取响应状态、响应头、响应体。
- 关闭客户端:
client.close();(当不再需要进行任何请求时,关闭客户端以释放底层资源)
B. package:http 的抽象与便利性
尽管 dart:io.HttpClient 提供了强大的底层控制能力,但在实际应用开发中,直接使用它会显得有些繁琐。例如,它没有内置的方便方法来处理 JSON 编解码,也缺乏高级的错误处理机制。为了简化 HTTP 请求的开发,Dart 社区普遍推荐使用 package:http。
package:http 是一个高级的、易于使用的 HTTP 客户端库,它在 dart:io.HttpClient 之上提供了一层更友好的抽象。
1. Client 接口与 IOClient
package:http 的核心是 http.Client 接口。它定义了 get, post, put, delete, head, patch, read, readBytes 等方法,返回 http.Response 或 String/List<int>。
IOClient 是 http.Client 接口的一个具体实现,它内部封装了 dart:io.HttpClient。在 Flutter 应用中,如果你只是简单地 import 'package:http/http.dart' as http; 并使用 http.get, http.post 等顶级函数,那么底层使用的就是 IOClient 的实例。
import 'package:http/http.dart' as http;
import 'dart:convert';
Future<void> makeHttpRequestWithPackageHttp() async {
final uri = Uri.parse('https://jsonplaceholder.typicode.com/posts/1');
try {
final response = await http.get(uri);
if (response.statusCode == 200) {
print('Response body: ${response.body}');
final Map<String, dynamic> data = jsonDecode(response.body);
print('Title: ${data['title']}');
} else {
print('Request failed with status: ${response.statusCode}.');
}
} catch (e) {
print('Error: $e');
}
}
2. BaseClient 与 with 模式
package:http 提供了一个抽象类 BaseClient,它实现了 http.Client 接口,并提供了一个 send 方法的抽象。通过继承 BaseClient 并重写 send 方法,我们可以非常方便地实现自定义的请求处理逻辑,例如添加全局 Header、日志记录、错误重试等。这种模式是实现请求拦截器的基础。
import 'package:http/http.dart' as http;
import 'package:http/src/base_request.dart';
import 'package:http/src/response.dart';
import 'package:http/src/streamed_response.dart';
class MyCustomClient extends http.BaseClient {
final http.Client _inner;
MyCustomClient(this._inner);
@override
Future<http.StreamedResponse> send(http.BaseRequest request) {
// 在这里可以修改请求
request.headers['X-Custom-Header'] = 'MyValue';
print('Sending request to ${request.url} with custom header.');
// 将请求发送给内部的 Client
return _inner.send(request);
}
}
Future<void> makeRequestWithCustomClient() async {
final client = MyCustomClient(http.Client()); // 包装默认的 http.Client
try {
final response = await client.get(Uri.parse('https://jsonplaceholder.typicode.com/posts/1'));
print('Status: ${response.statusCode}');
print('Body: ${response.body.substring(0, 100)}...'); // 打印部分响应体
} finally {
client.close(); // 记得关闭客户端
}
}
3. 为什么选择 package:http?
- 简洁性: 更简单的 API,减少了底层
Stream和Future的直接操作。 - 灵活性:
BaseClient模式为实现各种拦截器和装饰器提供了便利。 - 可测试性: 抽象的
Client接口使得模拟 HTTP 请求变得容易。 - 生态系统: 大多数第三方库和框架(如
Dio等)都建立在类似的思想之上,或者提供了与package:http兼容的接口。
C. HttpClient 的配置与安全性
dart:io.HttpClient 提供了丰富的配置选项,以满足不同的网络环境和安全需求。
1. 证书验证 (badCertificateCallback)
在处理 HTTPS 请求时,HttpClient 会对服务器证书进行验证。如果证书无效(例如自签名证书或过期证书),默认情况下会抛出异常。通过设置 badCertificateCallback,我们可以自定义证书验证逻辑。
import 'dart:io';
HttpClient createInsecureHttpClient() {
final client = HttpClient();
client.badCertificateCallback = (X509Certificate cert, String host, int port) {
// 仅用于开发环境,生产环境应严格验证证书
print('Bad certificate callback for host: $host, port: $port');
print('Issuer: ${cert.issuer}');
print('Subject: ${cert.subject}');
// 接受所有不安全证书(不推荐用于生产环境)
return true;
};
return client;
}
2. 代理设置 (findProxy)
HttpClient 支持通过代理服务器发送请求。findProxy 回调函数允许你根据请求的 URI 动态地返回代理字符串。
import 'dart:io';
HttpClient createProxyHttpClient() {
final client = HttpClient();
client.findProxy = (Uri uri) {
// 对于所有请求,使用本地 SOCKS5 代理
// 生产环境应根据实际情况配置代理规则
return HttpClient.findProxyFromEnvironment(uri, environment: {
'http_proxy': 'http://127.0.0.1:8888',
'https_proxy': 'http://127.0.0.1:8888',
// 'no_proxy': 'localhost,127.0.0.1' // 不需要代理的地址
});
// 或者硬编码代理
// return 'PROXY 127.0.0.1:8888';
};
return client;
}
3. 连接池与超时
maxConnectionsPerHost: 控制每个主机允许的最大并发连接数,默认为 5。connectionTimeout: 设置建立连接的超时时间。idleTimeout: 设置连接空闲超时时间,超过此时间空闲连接将被关闭。
这些配置都直接影响 HttpClient 的性能和稳定性。
import 'dart:io';
HttpClient createConfiguredHttpClient() {
final client = HttpClient()
..connectionTimeout = const Duration(seconds: 10) // 连接超时10秒
..idleTimeout = const Duration(seconds: 30) // 空闲连接30秒后关闭
..maxConnectionsPerHost = 10; // 每个主机最大10个并发连接
return client;
}
理解了 HttpClient 和 package:http 的基础,我们就可以更好地理解 DevTools 如何观察这些请求,以及如何在它们之上构建我们的拦截逻辑。
III. DevTools 网络分析器:您的网络侦察兵
DevTools 是一个强大的工具套件,旨在帮助 Dart 和 Flutter 开发者进行性能分析、调试、内存检查、UI 布局检查等。其中的网络分析器是专门用于监控和调试应用程序网络活动的模块。
A. DevTools 概述:Flutter/Dart 应用的性能与调试工具套件
DevTools 提供了一系列独立的工具,例如:
- Inspector: 检查 Flutter UI 布局和渲染树。
- Performance: 记录和分析应用程序的 CPU 使用率、帧渲染时间。
- Memory: 跟踪内存分配和垃圾回收,检测内存泄漏。
- Debugger: 传统的断点调试器。
- Network: 监控和分析所有 HTTP/HTTPS 网络请求。
- Logging: 查看应用程序的日志输出。
- CPU Profiler: 深入分析 CPU 调用栈。
DevTools 通常通过浏览器访问,连接到正在运行的 Dart/Flutter 应用程序的 VM Service。
B. 网络分析器的功能与界面
当你在 DevTools 中打开网络分析器时,它会显示一个实时更新的请求列表,以及每个请求的详细信息。
1. 请求列表:URL, 方法, 状态, 大小, 时间
主界面是一个表格,显示了所有捕获到的网络请求的关键信息:
- URL: 请求的目标地址。
- Method: HTTP 请求方法(GET, POST, PUT, DELETE 等)。
- Status: HTTP 响应状态码(200 OK, 404 Not Found, 500 Internal Server Error 等)。
- Type: 响应内容的类型(例如
application/json,image/png)。 - Size: 响应体的大小。
- Duration: 从请求开始到接收到完整响应的时间。
- Timeline: 一个迷你时间轴,直观显示请求的开始、持续和结束。
2. 请求详情:Header, Body, Cookies, Timing
点击列表中的任何一个请求,右侧会展开一个详细信息面板,通常包含以下标签页:
- Headers: 显示请求头(Request Headers)和响应头(Response Headers)。你可以检查
Authorization,Content-Type,User-Agent等关键信息。 - Request: 如果请求有请求体(如 POST 或 PUT),这里会显示请求体的内容。对于 JSON 或表单数据,通常会以可读的格式呈现。
- Response: 显示响应体的原始内容。对于 JSON、XML 或文本,可以直接查看;对于图片或其他二进制数据,可能会显示为十六进制或提示下载。
- Cookies: 显示请求和响应中涉及的 Cookie。
- Timing: 提供请求生命周期的详细时间分解,包括 DNS 解析、连接建立、发送请求、等待响应、接收响应等阶段的时间,有助于诊断网络延迟问题。
3. 性能指标:吞吐量, 延迟
网络分析器通常还会提供一些全局的性能指标,如:
- 总请求数: 在监控期间发出的所有请求数量。
- 总数据传输量: 上传和下载的数据总量。
- 平均延迟: 所有请求的平均响应时间。
这些指标有助于从宏观层面评估应用程序的网络性能。
C. DevTools 如何“看到”网络请求
DevTools 能够捕获 Dart 应用程序中的网络请求,其核心机制是基于 Dart VM Service Protocol。
1. dart:developer 或 VM Service API
Dart VM Service 是 Dart 虚拟机提供的一个调试和分析接口。它允许外部工具(如 DevTools)与正在运行的 Dart 应用程序进行通信,查询其状态、执行操作并监听事件。
dart:developer 库提供了一组函数,允许应用程序主动向 DevTools 发送信息(例如 log 函数)。但对于网络请求,DevTools 并不依赖于应用程序主动报告。相反,Dart VM 本身会发出关于 dart:io.HttpClient 实例的网络活动事件。
2. 事件流:HttpClient 内部事件的捕获
当 Dart 应用程序使用 dart:io.HttpClient 发起网络请求时,HttpClient 的内部实现会在关键时刻(如请求开始、请求头发送、响应头接收、响应体接收、请求结束等)发出特定的事件。DevTools 通过监听 Dart VM Service Protocol 提供的这些事件流,来捕获并聚合这些信息,最终在网络分析器中展示出来。
这意味着什么? DevTools 观察的是 dart:io.HttpClient 实例 实际 进行的网络通信。
- 如果你的应用程序直接使用
dart:io.HttpClient,DevTools 将直接捕获这些请求。 - 如果你的应用程序使用
package:http的IOClient(或任何基于dart:io.HttpClient实现的http.Client),DevTools 依然会捕获这些请求,因为IOClient最终会调用底层的dart:io.HttpClient。
3. 局限性:它是一个观察者,而非修改者
这是理解 DevTools 与代码层面拦截之间关系的关键点:
- DevTools 是一个被动的观察者。它记录的是
dart:io.HttpClient实际发送到网络和从网络接收到的数据。它无法在运行时修改请求或响应的内容。 - 它不“看到”应用程序内部的逻辑。如果你的应用程序在调用
HttpClient之前或之后对请求/响应数据进行了处理(例如,加密请求体,解密响应体),DevTools 看到的是处理之前(发送前)或处理之后(接收后)的原始网络数据,而不是应用程序内部的中间状态。 - 缓存的影响:如果你的应用程序实现了一个缓存机制,并且请求被缓存命中,导致
dart:io.HttpClient根本没有发出实际的网络请求,那么 DevTools 也不会记录这个请求,因为它从未到达网络层。
因此,DevTools 提供的是对 实际网络通信 的真实写照。它无法直接帮助你调试应用程序内部的业务逻辑或拦截器逻辑,但它能验证你的拦截器是否按预期改变了最终的网络请求或响应。
IV. 解密“拦截”:DevTools 与代码层面的不同含义
“拦截”这个词在不同的语境下有不同的含义,尤其是在 DevTools 和应用程序代码层面。明确这两者之间的区别对于正确理解和实现网络请求处理至关重要。
A. DevTools 的“拦截”:观察与记录
如前所述,DevTools 网络分析器所做的“拦截”实际上是观察和记录。它通过监听 Dart VM 内部的事件流来捕获 HttpClient 的网络活动。
- 观察对象: 是应用程序通过
dart:io.HttpClient与外部服务器进行的实际网络交互。 - 能力: 记录请求的 URL、方法、头、体、响应状态、响应头、响应体、时间等信息。
- 目的: 提供一个可视化的界面来监控网络流量,帮助开发者诊断网络相关的问题,如请求失败、响应延迟、数据格式错误等。
- 局限性: DevTools 不会修改任何请求或响应,它无法在网络请求发送前或接收后,在应用程序层面插入自定义逻辑。它只是一个被动的监听器。
B. 代码层面的“拦截”:修改、增强或阻止
与 DevTools 的被动观察不同,我们在应用程序代码层面所谈论的“拦截”是一个主动的过程。它指的是在网络请求被发送出去之前,或在网络响应被应用程序处理之前,插入自定义的代码逻辑,从而:
- 修改请求: 例如添加认证令牌、设置自定义 User-Agent、修改请求体格式。
- 增强请求: 例如实现请求重试、添加缓存逻辑、进行请求签名。
- 修改响应: 例如解密响应体、统一错误格式、处理特定的响应头。
- 阻止请求: 如果请求数据不合法或已被缓存,可以不发送实际的网络请求。
- 日志记录: 在请求发送前和响应接收后记录详细信息。
1. 为什么需要代码层面的拦截?
代码层面的拦截器模式带来了诸多好处,是构建健壮、可维护的网络层的核心策略:
- 关注点分离 (Separation of Concerns): 将网络请求的横切关注点(如认证、日志、缓存、错误处理)从业务逻辑中抽离出来,使业务代码更专注于自身的功能。
- 代码复用: 一旦实现了一个拦截器,它就可以应用于所有或特定的网络请求,避免了在每个请求发起处编写重复代码。
- 可维护性: 当需要修改认证逻辑或日志格式时,只需修改拦截器,而无需修改所有使用网络请求的业务代码。
- 可扩展性: 可以通过组合不同的拦截器来构建复杂的网络行为,并且易于添加新的拦截功能。
- 统一性: 确保所有网络请求都遵循相同的规范和处理流程。
2. 常见的拦截场景:认证、日志、缓存、重试、错误处理
以下是一些典型的应用场景,它们都通过代码层面的拦截器来实现:
- 认证 (Authentication): 自动在每个请求中添加
Authorization头(如 Bearer Token)。在 Token 过期时,可以自动刷新 Token 并重试请求。 - 日志 (Logging): 记录请求的 URL、方法、头、体,以及响应的状态码、头、体和耗时,用于调试和监控。
- 缓存 (Caching): 在发送请求前检查是否有缓存数据,如果有则直接返回缓存;在收到响应后将数据存入缓存。
- 重试 (Retry): 当网络请求因瞬时错误(如网络不稳定、服务器过载)失败时,自动进行重试。
- 错误处理 (Error Handling): 统一处理 HTTP 状态码(如 401 Unauthorized, 403 Forbidden, 500 Internal Server Error),将其转换为应用程序特定的错误类型。
- 数据转换 (Data Transformation): 对请求体进行加密或压缩,对响应体进行解密或解压缩。
- 请求签名 (Request Signing): 对请求参数进行签名,以确保数据完整性和来源合法性。
理解了 DevTools 的观察角色和代码层面拦截的主动操作,我们就可以着手设计和实现 Dart 应用程序中的拦截器了。DevTools 将成为我们验证这些拦截器是否按预期工作的有力辅助工具。
V. Dart HttpClient 请求的编程拦截策略与实现
在 Dart 中实现网络请求拦截,主要有两种策略:基于 dart:io.HttpClient 的底层包装,以及基于 package:http 的高级抽象。通常,我们更推荐使用 package:http 的方式,因为它更简洁、更灵活,并且与 Flutter 应用的集成更为平滑。
A. 基于 dart:io.HttpClient 的底层拦截
直接操作 dart:io.HttpClient 提供了最大的灵活性,但实现起来也相对复杂。它通常通过包装 HttpClient 实例,并重写其创建请求的方法来实现。
1. 自定义 HttpClient 包装器
我们可以创建一个自定义类,它持有一个 HttpClient 实例,并暴露类似 HttpClient 的方法,但在调用底层实例之前或之后插入我们的逻辑。
import 'dart:io';
import 'dart:convert';
// 定义一个简单的日志函数
void _log(String message) {
print('[HttpClientInterceptor] $message');
}
class InterceptingHttpClient implements HttpClient {
final HttpClient _innerClient;
InterceptingHttpClient(this._innerClient);
// 包装核心方法,例如 getUrl, postUrl 等
@override
Future<HttpClientRequest> getUrl(Uri url) => _interceptRequest(_innerClient.getUrl(url));
@override
Future<HttpClientRequest> postUrl(Uri url) => _interceptRequest(_innerClient.postUrl(url));
// 你需要为所有你需要拦截的 HttpClientRequest 方法提供包装
// 这里仅为示例,省略了大部分
Future<HttpClientRequest> _interceptRequest(Future<HttpClientRequest> originalRequestFuture) async {
final originalRequest = await originalRequestFuture;
_log('Request started: ${originalRequest.method} ${originalRequest.uri}');
_log('Request headers: ${originalRequest.headers.toString()}');
// 可以在这里修改请求头
originalRequest.headers.add('X-App-Id', 'MyApp-123');
originalRequest.headers.add('Authorization', 'Bearer my_token_from_storage');
// 返回一个自定义的 HttpClientRequestWrapper,以便在写入请求体和关闭请求时进行拦截
return _InterceptingHttpClientRequest(originalRequest);
}
@override
Stream<HttpClientResponse> autoUncompress(HttpClientResponse response) => _innerClient.autoUncompress(response);
@override
void close({bool force = false}) => _innerClient.close(force: force);
// ... 更多 HttpClient 接口方法的包装,通常只是简单转发
// 属性转发
@override
Duration? connectionTimeout; // 属性需要手动转发或在构造函数中设置
// ... 其他属性
@override
bool autoUncompress = true; // 示例
@override
set badCertificateCallback(bool Function(X509Certificate cert, String host, int port)? callback) {
_innerClient.badCertificateCallback = callback;
}
@override
bool Function(X509Certificate cert, String host, int port)? get badCertificateCallback => _innerClient.badCertificateCallback;
@override
set authenticate(Future<bool> Function(Uri url, String scheme, String? realm)? f) {
_innerClient.authenticate = f;
}
@override
Future<bool> Function(Uri url, String scheme, String? realm)? get authenticate => _innerClient.authenticate;
@override
set findProxy(String Function(Uri url)? f) {
_innerClient.findProxy = f;
}
@override
String Function(Uri url)? get findProxy => _innerClient.findProxy;
@override
int maxConnectionsPerHost = 5;
@override
set authenticateProxy(Future<bool> Function(String host, String scheme, String? realm)? f) {
_innerClient.authenticateProxy = f;
}
@override
Future<bool> Function(String host, String scheme, String? realm)? get authenticateProxy => _innerClient.authenticateProxy;
@override
Duration? idleTimeout;
@override
set keyLog(Function(String line)? callback) {
_innerClient.keyLog = callback;
}
@override
Function(String line)? get keyLog => _innerClient.keyLog;
@override
bool preserveHeaderCase = false;
}
// 包装 HttpClientRequest 以拦截请求体写入和请求关闭
class _InterceptingHttpClientRequest implements HttpClientRequest {
final HttpClientRequest _innerRequest;
final BytesBuilder _bodyBytes = BytesBuilder(); // 用于存储请求体
_InterceptingHttpClientRequest(this._innerRequest);
@override
HttpHeaders get headers => _innerRequest.headers;
@override
String get method => _innerRequest.method;
@override
Uri get uri => _innerRequest.uri;
@override
void add(List<int> data) {
_bodyBytes.add(data); // 存储数据
_innerRequest.add(data); // 转发数据
}
@override
void addError(Object error, [StackTrace? stackTrace]) {
_innerRequest.addError(error, stackTrace);
}
@override
Future<HttpClientResponse> close() async {
final response = await _innerRequest.close();
// 在这里可以读取请求体,如果需要
final requestBody = utf8.decode(_bodyBytes.takeBytes(), allowMalformed: true);
_log('Request body for ${method} ${uri}: $requestBody');
// 包装 HttpClientResponse 以拦截响应
return _InterceptingHttpClientResponse(response);
}
@override
Future<void> flush() => _innerRequest.flush();
@override
void write(Object? obj) {
// 这里需要处理 obj 的类型,转换为 List<int>
final data = utf8.encode(obj.toString()); // 假设是字符串
_bodyBytes.add(data);
_innerRequest.write(obj);
}
@override
void writeAll(Iterable<Object?> objects, [String separator = '']) {
final data = utf8.encode(objects.join(separator));
_bodyBytes.add(data);
_innerRequest.writeAll(objects, separator);
}
@override
void writeCharCode(int charCode) {
final data = [charCode];
_bodyBytes.add(data);
_innerRequest.writeCharCode(charCode);
}
}
// 包装 HttpClientResponse 以拦截响应体读取
class _InterceptingHttpClientResponse implements HttpClientResponse {
final HttpClientResponse _innerResponse;
final BytesBuilder _bodyBytes = BytesBuilder();
_InterceptingHttpClientResponse(this._innerResponse) {
// 监听响应体流
_innerResponse.listen(
(data) {
_bodyBytes.add(data); // 存储响应体
},
onDone: () {
final responseBody = utf8.decode(_bodyBytes.takeBytes(), allowMalformed: true);
_log('Response received for ${_innerResponse.request!.method} ${_innerResponse.request!.uri}');
_log('Response status: ${_innerResponse.statusCode}');
_log('Response headers: ${_innerResponse.headers.toString()}');
_log('Response body: $responseBody');
},
onError: (error, stackTrace) {
_log('Response error: $error');
}
);
}
@override
int get contentLength => _innerResponse.contentLength;
@override
HttpClientRequest? get request => _innerResponse.request;
@override
int get statusCode => _innerResponse.statusCode;
@override
HttpHeaders get headers => _innerResponse.headers;
// 必须转发所有 Stream<List<int>> 的方法
@override
Stream<List<int>> asBroadcastStream({void Function(StreamSubscription<List<int>> subscription)? onListen, void Function(StreamSubscription<List<int>> subscription)? onCancel}) {
return _innerResponse.asBroadcastStream(onListen: onListen, onCancel: onCancel);
}
@override
Future<bool> contains(Object? element) => _innerResponse.contains(element);
// ... 更多 Stream<List<int>> 的方法,需要全部转发
// 这里只提供部分示例,实际应用中需要实现所有接口方法
@override
Future<int> get length => _innerResponse.length;
@override
Future<List<int>> toList() => _innerResponse.toList();
@override
StreamSubscription<List<int>> listen(void Function(List<int> event)? onData, {Function? onError, void Function()? onDone, bool? cancelOnError}) {
// 注意:这里的 listen 应该转发给 _innerResponse,但同时我们已经在构造函数中监听了。
// 这可能导致响应体被读取多次或逻辑混乱。
// 更安全的做法是,在 _InterceptingHttpClientResponse 内部维护一个经过处理的 StreamController,
// 然后将 _innerResponse 的数据推送到这个 Controller,再由它提供给外部。
// 为了简单起见,这里直接转发,并假设外部只监听一次。
return _innerResponse.listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError);
}
// 其他 HttpClientResponse 属性和方法也需要转发
@override
X509Certificate? get certificate => _innerResponse.certificate;
@override
List<Cookie> get cookies => _innerResponse.cookies;
@override
String? get reasonPhrase => _innerResponse.reasonPhrase;
@override
Duration? get connectionInfo => _innerResponse.connectionInfo;
@override
bool get isRedirect => _innerResponse.isRedirect;
@override
bool get persistentConnection => _innerResponse.persistentConnection;
@override
Future<HttpClientResponse> redirect([String? method, Uri? url, bool? followLoops]) => _innerResponse.redirect(method, url, followLoops);
@override
List<RedirectInfo> get redirects => _innerResponse.redirects;
}
void main() async {
// 使用我们自定义的拦截器客户端
final interceptingClient = InterceptingHttpClient(HttpClient());
try {
// 示例 GET 请求
final requestGet = await interceptingClient.getUrl(Uri.parse('https://jsonplaceholder.typicode.com/posts/1'));
final responseGet = await requestGet.close();
await responseGet.drain(); // 确保读取完响应体
print('GET request finished.');
// 示例 POST 请求
final requestPost = await interceptingClient.postUrl(Uri.parse('https://jsonplaceholder.typicode.com/posts'));
requestPost.headers.contentType = ContentType.json;
final Map<String, dynamic> postData = {'title': 'foo', 'body': 'bar', 'userId': 1};
requestPost.write(jsonEncode(postData));
final responsePost = await requestPost.close();
await responsePost.drain(); // 确保读取完响应体
print('POST request finished.');
} catch (e) {
print('An error occurred: $e');
} finally {
interceptingClient.close();
}
}
这种底层包装的缺点:
- 繁琐: 需要手动包装
HttpClient和HttpClientRequest/HttpClientResponse的所有方法和属性,工作量巨大且容易出错。 - 复杂的状态管理: 尤其是在处理
HttpClientResponse的Stream时,需要确保响应体只被读取一次,或者正确地缓存和重放。 - 不兼容
package:http: 如果应用程序使用了package:http,这种方式无法直接集成。
因此,除非有非常特殊的底层控制需求,否则不推荐直接使用这种方式。
B. 基于 package:http 的高级拦截器模式
package:http 库的 BaseClient 抽象类为实现请求拦截器提供了优雅而强大的机制。这种模式通过装饰器或链式责任模式,将不同的拦截逻辑串联起来。
1. BaseClient 的扩展与链式调用
BaseClient 强制你实现一个 Future<StreamedResponse> send(BaseRequest request) 方法。这个方法是所有 HTTP 请求的入口点。通过包装一个内部的 http.Client 实例,你可以在请求发送之前和响应返回之后执行自定义逻辑。
我们可以构建一个拦截器链,每个拦截器都是一个 BaseClient 的子类,它包装了链中的下一个 BaseClient。
import 'package:http/http.dart' as http;
import 'package:http/src/base_request.dart';
import 'package:http/src/response.dart';
import 'package:http/src/streamed_response.dart';
import 'dart:convert';
import 'dart:async';
// 1. 日志拦截器
class LoggingClient extends http.BaseClient {
final http.Client _inner;
LoggingClient(this._inner);
@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
final stopwatch = Stopwatch()..start();
print('--- Request Log ---');
print('Method: ${request.method}');
print('URL: ${request.url}');
print('Headers: ${request.headers}');
// 读取请求体(如果存在)
String requestBody = '';
if (request.contentLength > 0 && request is http.Request) {
requestBody = request.body; // 对于 http.Request,可以直接获取 body
print('Request Body: $requestBody');
} else if (request.contentLength > 0) {
// 对于 http.StreamedRequest,需要先读取流,再重新创建请求
// 这是一个复杂场景,通常通过复制请求来实现,这里简化处理
print('Request Body: (Streamed body not easily logged here without buffering)');
}
try {
final response = await _inner.send(request);
stopwatch.stop();
print('--- Response Log ---');
print('Status: ${response.statusCode}');
print('Reason Phrase: ${response.reasonPhrase}');
print('Headers: ${response.headers}');
// 读取响应体
final responseBody = await response.stream.bytesToString();
print('Response Body: $responseBody');
print('Duration: ${stopwatch.elapsedMilliseconds}ms');
print('-------------------');
// 返回新的 StreamedResponse,因为原始 stream 已经被读取
return http.StreamedResponse(
Stream.value(utf8.encode(responseBody)),
response.statusCode,
contentLength: responseBody.length,
request: response.request,
headers: response.headers,
isRedirect: response.isRedirect,
persistentConnection: response.persistentConnection,
reasonPhrase: response.reasonPhrase,
);
} catch (e) {
stopwatch.stop();
print('--- Error Log ---');
print('Error: $e');
print('Duration: ${stopwatch.elapsedMilliseconds}ms');
print('-------------------');
rethrow;
}
}
}
// 2. 认证拦截器
class AuthClient extends http.BaseClient {
final http.Client _inner;
final String _authToken;
AuthClient(this._inner, this._authToken);
@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
// 复制请求以添加头,因为原始请求可能是不可变的
final newRequest = http.Request(request.method, request.url)
..headers.addAll(request.headers)
..headers['Authorization'] = 'Bearer $_authToken';
if (request is http.Request) {
newRequest.bodyBytes = request.bodyBytes;
} else if (request is http.StreamedRequest) {
// 对于 streamed request, 需要处理 stream
// 这是一个更复杂的问题,通常需要将 stream 转换为 Future<List<int>>
// 然后再设置给新的请求。这里为了简化,假设大部分是 Request 类型。
// 实际生产中可能需要更精细的流处理。
newRequest.bodyBytes = await request.finalize().toBytes();
}
return _inner.send(newRequest);
}
}
// 3. 重试拦截器
class RetryClient extends http.BaseClient {
final http.Client _inner;
final int _maxRetries;
final Duration _delay;
RetryClient(this._inner, {int maxRetries = 3, Duration delay = const Duration(seconds: 1)})
: _maxRetries = maxRetries,
_delay = delay;
@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
int retries = 0;
http.Request? originalRequest; // 用于存储原始请求的副本,以便重试
// 复制原始请求,以便多次发送
if (request is http.Request) {
originalRequest = http.Request(request.method, request.url)
..headers.addAll(request.headers)
..bodyBytes = request.bodyBytes;
} else if (request is http.StreamedRequest) {
// 对于 StreamedRequest,需要在每次重试时重新创建 stream
// 这需要从原始 stream 中读取所有数据,然后将其存储起来,
// 并在每次重试时使用 Stream.fromIterable 创建新的 stream。
// 这里为了简单,我们假设请求体不大,可以一次性读取。
final bodyBytes = await request.finalize().toBytes();
originalRequest = http.Request(request.method, request.url)
..headers.addAll(request.headers)
..bodyBytes = bodyBytes;
}
while (retries < _maxRetries) {
try {
final currentRequest = originalRequest != null
? (http.Request(originalRequest.method, originalRequest.url)
..headers.addAll(originalRequest.headers)
..bodyBytes = originalRequest.bodyBytes)
: request; // 如果 originalRequest 为 null,则直接使用传入的 request (第一次尝试)
final response = await _inner.send(currentRequest);
// 如果状态码是 5xx 或 429 (Too Many Requests),则考虑重试
if (response.statusCode >= 500 || response.statusCode == 429) {
print('Request to ${request.url} failed with status ${response.statusCode}. Retrying (${retries + 1}/${_maxRetries})...');
retries++;
await Future.delayed(_delay); // 等待一段时间再重试
// 重要:在重试之前,需要确保原始响应的流已经被完全消费或关闭
// 否则会造成连接泄漏。对于 StreamedResponse,通常需要 drain()。
await response.stream.drain();
continue; // 继续下一次循环,尝试重试
}
return response; // 成功或非重试状态码,返回响应
} catch (e) {
print('Request to ${request.url} failed with error: $e. Retrying (${retries + 1}/${_maxRetries})...');
retries++;
await Future.delayed(_delay);
continue; // 继续下一次循环,尝试重试
}
}
// 如果达到最大重试次数仍未成功,则抛出异常
throw http.ClientException('Failed to send request to ${request.url} after $_maxRetries retries.');
}
}
// 组装拦截器链
http.Client createClientWithInterceptors() {
http.Client client = http.Client(); // 最底层的客户端
// 从内到外包装,执行顺序从外到内
// 例如:RetryClient -> AuthClient -> LoggingClient -> http.Client
// 请求从 LoggingClient -> AuthClient -> RetryClient -> http.Client 发出
// 响应从 http.Client -> RetryClient -> AuthClient -> LoggingClient 返回
client = RetryClient(client, maxRetries: 2); // 先处理重试
client = AuthClient(client, 'your_super_secret_token'); // 再添加认证
client = LoggingClient(client); // 最后日志记录
return client;
}
void main() async {
final client = createClientWithInterceptors();
try {
// 模拟一个请求,例如到一个不存在的地址,触发日志和重试(如果网络错误)
final response = await client.get(Uri.parse('https://jsonplaceholder.typicode.com/posts/1')); // 这是一个会成功的请求
// final response = await client.get(Uri.parse('https://httpstat.us/503')); // 模拟一个 503 错误,触发重试
// final response = await client.get(Uri.parse('http://nonexistent-domain-xyz.com')); // 模拟一个网络错误,触发重试
print('nFinal response status: ${response.statusCode}');
print('Final response body (first 100 chars): ${response.body.substring(0, response.body.length > 100 ? 100 : response.body.length)}...');
} catch (e) {
print('nCaught final error: $e');
} finally {
client.close(); // 记得关闭客户端
}
}
with 关键字与混入 (Mixins):
在 Dart 中,我们也可以使用 with 关键字来将拦截器作为混入应用到 BaseClient 上。这种模式在某些情况下可以使代码更简洁,但需要拦截器设计成混入。
例如,如果你有多个 BaseClient 包装器,你可以这样组合它们:
class MyAppClient extends http.BaseClient with LoggingClientMixin, AuthClientMixin {
final http.Client _inner; // 内部持有的客户端
MyAppClient(this._inner);
@override
Future<http.StreamedResponse> send(http.BaseRequest request) {
// 混入会在这里处理逻辑,并最终调用 _inner.send(request)
// 具体的实现方式需要混入类本身的设计来决定如何链式调用
// 这通常需要混入本身也接受一个 Client 作为构造参数或通过其他方式获取。
// 混入模式对于复杂链式调用不如包装器直接清晰。
return _inner.send(request);
}
}
然而,对于拦截器链,直接的装饰器模式(如上面的 client = LoggingClient(client);)通常更清晰,因为它明确了调用的顺序和包装关系。
2. http.Client 装饰器模式
我们上面实现的 LoggingClient, AuthClient, RetryClient 都是 http.Client 的装饰器模式的体现。每个装饰器都接受一个 http.Client 作为其构造函数参数,并在其 send 方法中调用被包装的 Client 的 send 方法,从而形成一个处理链。
3. 拦截器链的构建与管理
-
顺序与依赖: 拦截器的顺序非常重要。例如,认证拦截器应该在日志拦截器之前添加认证头,这样日志拦截器才能记录带认证头的请求。重试拦截器应该在认证拦截器之后,以便在重试时使用正确的认证信息。
- 请求流向:
Client A->Client B->Client C->IOClient(实际网络请求) - 响应流向:
IOClient->Client C->Client B->Client A
在上面的
createClientWithInterceptors()函数中,我们按照“最外层处理最通用或最先发生”的原则来组织:RetryClient(client): 最先处理可能的网络错误和重试。AuthClient(client, ...): 接着添加认证信息。LoggingClient(client): 最后记录请求和响应的完整信息。
这意味着当一个请求发出时,它会先经过
LoggingClient(记录原始请求,然后将请求传递给AuthClient),然后经过AuthClient(添加认证头,然后将请求传递给RetryClient),然后经过RetryClient(处理重试逻辑,然后将请求传递给底层的http.Client),最终由http.Client发送。响应则逆序返回。 - 请求流向:
-
错误处理在链中的传播: 如果链中的某个拦截器抛出异常,异常会沿着调用栈向上冒泡,直到被某个拦截器或应用程序捕获。这意味着你可以在任何拦截器中捕获和处理错误,也可以选择让错误继续传播。
VI. 深入代码:构建一个完整的 package:http 拦截器体系
为了更好地展示拦截器模式的强大和灵活性,我们将构建一个更通用、更易于扩展的拦截器体系。这个体系将包含一个通用的拦截器接口和多个具体的拦截器实现,并通过一个 InterceptedClient 类来管理拦截器链。
A. 定义拦截器接口
为了让我们的拦截器体系更加模块化和可扩展,我们可以定义明确的拦截器接口。
import 'package:http/http.dart' as http;
import 'package:http/src/base_request.dart';
import 'package:http/src/response.dart';
import 'package:http/src/streamed_response.dart';
import 'dart:async';
/// 拦截器链的处理器函数类型
typedef HttpHandler = Future<http.StreamedResponse> Function(http.BaseRequest request);
/// 核心拦截器接口
abstract class Interceptor {
/// 处理请求的方法
/// [request] 原始请求或上一个拦截器处理后的请求
/// [handler] 链中的下一个处理器(可能是另一个拦截器或最终的 http.Client)
Future<http.StreamedResponse> intercept(http.BaseRequest request, HttpHandler handler);
}
这个 Interceptor 接口非常通用:它接收一个 http.BaseRequest 和一个 HttpHandler。handler 代表了链中后续的所有处理步骤,包括最终发送请求的 http.Client。拦截器可以在调用 handler(request) 之前修改请求,也可以在 handler(request) 返回的 Future 完成之后处理响应。
B. 实现一个通用的 HttpClientInterceptor 基类
为了方便管理和应用这些拦截器,我们需要一个能够将它们串联起来的 http.Client 实现。
class InterceptedClient extends http.BaseClient {
final http.Client _inner;
final List<Interceptor> _interceptors;
InterceptedClient({
required http.Client inner,
List<Interceptor> interceptors = const [],
}) : _inner = inner,
_interceptors = interceptors;
@override
Future<http.StreamedResponse> send(http.BaseRequest request) {
// 构建拦截器链
HttpHandler handler = (req) => _inner.send(req); // 链的末端是实际的 http.Client
// 从后往前遍历拦截器列表,构建链式调用
// 这样,列表中的第一个拦截器将是第一个被调用的(最外层)
for (var i = _interceptors.length - 1; i >= 0; i--) {
final interceptor = _interceptors[i];
final nextHandler = handler; // 捕获当前 handler
handler = (req) => interceptor.intercept(req, nextHandler);
}
// 调用最外层的拦截器,开始处理请求
return handler(request);
}
@override
void close() {
_inner.close();
super.close();
}
}
InterceptedClient 的 send 方法是核心:它通过循环构建了一个 HttpHandler 链。当 handler(request) 被调用时,它会从列表中的第一个拦截器开始,逐级调用 intercept 方法,直到最终调用 _inner.send(request)。
C. 实现具体的拦截器
现在,让我们使用 Interceptor 接口来实现一些具体的拦截器。
1. AuthInterceptor (添加 Bearer Token)
class AuthInterceptor implements Interceptor {
final String _authToken;
AuthInterceptor(this._authToken);
@override
Future<http.StreamedResponse> intercept(http.BaseRequest request, HttpHandler handler) async {
// 克隆请求并添加认证头
final http.Request newRequest = http.Request(request.method, request.url)
..headers.addAll(request.headers)
..headers['Authorization'] = 'Bearer $_authToken';
// 如果是 http.Request 类型,可以复制 bodyBytes
if (request is http.Request) {
newRequest.bodyBytes = request.bodyBytes;
} else if (request is http.StreamedRequest) {
// 对于 StreamedRequest,需要将流读取出来并重新写入
newRequest.bodyBytes = await request.finalize().toBytes();
}
print('[AuthInterceptor] Added Authorization header.');
return handler(newRequest);
}
}
2. LogInterceptor (详细记录请求与响应)
class LogInterceptor implements Interceptor {
@override
Future<http.StreamedResponse> intercept(http.BaseRequest request, HttpHandler handler) async {
final stopwatch = Stopwatch()..start();
print('n--- [LogInterceptor] Request Start ---');
print('Method: ${request.method}');
print('URL: ${request.url}');
print('Headers: ${request.headers}');
// 复制请求体用于日志,因为原始流只能读取一次
final http.Request requestForLog = http.Request(request.method, request.url)
..headers.addAll(request.headers);
if (request is http.Request) {
requestForLog.bodyBytes = request.bodyBytes;
} else if (request is http.StreamedRequest) {
requestForLog.bodyBytes = await request.finalize().toBytes();
}
print('Request Body: ${utf8.decode(requestForLog.bodyBytes, allowMalformed: true)}');
print('-------------------------------------');
try {
// 创建一个新的 StreamedRequest 用于传递给下一个处理器
// 这里的关键是确保 requestForLog.bodyBytes 再次被提供给 handler
final http.StreamedRequest streamedRequestForHandler = http.StreamedRequest(request.method, request.url)
..headers.addAll(requestForLog.headers);
requestForLog.bodyBytes.forEach(streamedRequestForHandler.sink.add);
await streamedRequestForHandler.sink.close();
final response = await handler(streamedRequestForHandler);
stopwatch.stop();
print('--- [LogInterceptor] Response End ---');
print('Status: ${response.statusCode}');
print('Reason Phrase: ${response.reasonPhrase}');
print('Headers: ${response.headers}');
// 读取响应体用于日志,并重新构建响应
final responseBody = await response.stream.bytesToString();
print('Response Body: $responseBody');
print('Duration: ${stopwatch.elapsedMilliseconds}ms');
print('-----------------------------------');
// 返回一个新的 StreamedResponse,因为原始 stream 已经被读取
return http.StreamedResponse(
Stream.value(utf8.encode(responseBody)),
response.statusCode,
contentLength: responseBody.length,
request: response.request,
headers: response.headers,
isRedirect: response.isRedirect,
persistentConnection: response.persistentConnection,
reasonPhrase: response.reasonPhrase,
);
} catch (e) {
stopwatch.stop();
print('--- [LogInterceptor] Error End ---');
print('Error: $e');
print('Duration: ${stopwatch.elapsedMilliseconds}ms');
print('---------------------------------');
rethrow;
}
}
}
重要提示: 在 LogInterceptor 中,由于 http.BaseRequest 的 finalize() 方法会消耗请求体流,http.StreamedResponse 的 stream 也会消耗响应体流,因此为了能在日志中打印请求/响应体并同时将它们传递给下一个处理器或返回给调用者,我们必须:
- 对于请求体: 在
handler调用之前,将http.BaseRequest的体流完全读取出来(例如通过request.finalize().toBytes()),然后创建一个新的http.StreamedRequest,并将读取到的体数据重新写入新请求的sink。 - 对于响应体: 在
handler调用之后,将http.StreamedResponse的体流完全读取出来(例如通过response.stream.bytesToString()),然后创建一个新的http.StreamedResponse,并将读取到的体数据重新包装成一个新的Stream.value。
这些操作会带来额外的内存和 CPU 开销,尤其是在请求/响应体非常大的情况下。
3. RetryInterceptor (处理网络错误与特定状态码)
class RetryInterceptor implements Interceptor {
final int _maxRetries;
final Duration _delay;
final List<int> _retryableStatusCodes;
RetryInterceptor({
int maxRetries = 3,
Duration delay = const Duration(seconds: 1),
List<int>? retryableStatusCodes,
}) : _maxRetries = maxRetries,
_delay = delay,
_retryableStatusCodes = retryableStatusCodes ?? [429, 500, 502, 503, 504];
@override
Future<http.StreamedResponse> intercept(http.BaseRequest request, HttpHandler handler) async {
int retries = 0;
// 为了重试,我们需要一个可重复读取的请求体
List<int> requestBodyBytes = [];
if (request is http.Request) {
requestBodyBytes = request.bodyBytes;
} else if (request is http.StreamedRequest) {
requestBodyBytes = await request.finalize().toBytes();
}
while (retries < _maxRetries) {
try {
// 每次重试都创建一个新的 StreamedRequest,并写入体数据
final http.StreamedRequest currentRequest = http.StreamedRequest(request.method, request.url)
..headers.addAll(request.headers);
requestBodyBytes.forEach(currentRequest.sink.add);
await currentRequest.sink.close();
final response = await handler(currentRequest);
if (_retryableStatusCodes.contains(response.statusCode)) {
print('[RetryInterceptor] Request to ${request.url} failed with status ${response.statusCode}. Retrying (${retries + 1}/${_maxRetries})...');
retries++;
await Future.delayed(_delay);
await response.stream.drain(); // 消耗响应体以释放连接
continue;
}
return response; // 成功或非重试状态码
} catch (e) {
print('[RetryInterceptor] Request to ${request.url} failed with error: $e. Retrying (${retries + 1}/${_maxRetries})...');
retries++;
await Future.delayed(_delay);
continue;
}
}
throw http.ClientException('Failed to send request to ${request.url} after $_maxRetries retries.');
}
}
4. CacheInterceptor (简单的内存缓存)
// 简单的内存缓存
final Map<String, http.Response> _cache = {};
final Map<String, DateTime> _cacheExpiry = {};
final Duration _cacheDuration = const Duration(minutes: 5);
class CacheInterceptor implements Interceptor {
@override
Future<http.StreamedResponse> intercept(http.BaseRequest request, HttpHandler handler) async {
final String cacheKey = request.url.toString();
// 检查缓存
if (request.method == 'GET' && _cache.containsKey(cacheKey)) {
final expiryTime = _cacheExpiry[cacheKey];
if (expiryTime != null && DateTime.now().isBefore(expiryTime)) {
final cachedResponse = _cache[cacheKey]!;
print('[CacheInterceptor] Cache hit for ${request.url}. Returning cached response.');
// 将 Response 转换为 StreamedResponse 返回
return http.StreamedResponse(
Stream.value(cachedResponse.bodyBytes),
cachedResponse.statusCode,
contentLength: cachedResponse.contentLength,
request: cachedResponse.request,
headers: cachedResponse.headers,
isRedirect: cachedResponse.isRedirect,
persistentConnection: cachedResponse.persistentConnection,
reasonPhrase: cachedResponse.reasonPhrase,
);
} else {
print('[CacheInterceptor] Cache expired for ${request.url}. Invalidating cache.');
_cache.remove(cacheKey);
_cacheExpiry.remove(cacheKey);
}
}
// 如果没有缓存或缓存过期,则继续处理请求
print('[CacheInterceptor] No cache hit for ${request.url}. Sending request.');
final response = await handler(request);
// 如果是 GET 请求且状态码成功,则缓存响应
if (request.method == 'GET' && response.statusCode >= 200 && response.statusCode < 300) {
final responseBody = await response.stream.bytesToString();
final http.Response actualResponse = http.Response(
responseBody,
response.statusCode,
request: response.request,
headers: response.headers,
isRedirect: response.isRedirect,
persistentConnection: response.persistentConnection,
reasonPhrase: response.reasonPhrase,
);
_cache[cacheKey] = actualResponse;
_cacheExpiry[cacheKey] = DateTime.now().add(_cacheDuration);
print('[CacheInterceptor] Cached response for ${request.url}.');
// 返回新的 StreamedResponse
return http.StreamedResponse(
Stream.value(utf8.encode(responseBody)),
response.statusCode,
contentLength: responseBody.length,
request: response.request,
headers: response.headers,
isRedirect: response.isRedirect,
persistentConnection: response.persistentConnection,
reasonPhrase: response.reasonPhrase,
);
}
return response; // 非 GET 请求或失败响应,直接返回
}
}
D. 组装拦截器链与 HttpClient 配置
最后,我们将这些拦截器组装起来,创建一个可用的 http.Client 实例。
// main.dart 或应用程序的初始化文件
void main() async {
// 1. 创建底层的 http.Client 实例
// 可以在这里配置底层的 HttpClient,例如设置证书验证、代理等
// 如果你需要自定义 dart:io.HttpClient,可以像这样创建
// final ioClient = HttpClient();
// ioClient.badCertificateCallback = (cert, host, port) => true; // 仅用于开发测试
// final baseClient = http.IOClient(ioClient);
// 或者直接使用默认的 IOClient
final baseClient = http.Client();
// 2. 定义拦截器列表
// 拦截器在列表中的顺序决定了它们被调用的顺序
// 注意:请求会从列表的第一个拦截器开始处理,响应会逆序返回。
final List<Interceptor> interceptors = [
LogInterceptor(), // 记录请求和响应的详细信息
AuthInterceptor('my_secure_jwt_token_123'), // 添加认证头
RetryInterceptor(maxRetries: 2, delay: const Duration(seconds: 2)), // 失败时重试
CacheInterceptor(), // 简单的 GET 请求缓存
];
// 3. 使用 InterceptedClient 组装拦截器链
final http.Client client = InterceptedClient(
inner: baseClient,
interceptors: interceptors,
);
try {
print('--- Making first request (should trigger all interceptors and cache) ---');
final response1 = await client.get(Uri.parse('https://jsonplaceholder.typicode.com/posts/1'));
print('First request finished with status: ${response1.statusCode}');
print('Body preview: ${response1.body.substring(0, response1.body.length > 100 ? 100 : response1.body.length)}...n');
print('--- Making second request (should hit cache) ---');
final response2 = await client.get(Uri.parse('https://jsonplaceholder.typicode.com/posts/1'));
print('Second request finished with status: ${response2.statusCode}');
print('Body preview: ${response2.body.substring(0, response2.body.length > 100 ? 100 : response2.body.length)}...n');
print('--- Making a POST request (should not hit cache) ---');
final postResponse = await client.post(
Uri.parse('https://jsonplaceholder.typicode.com/posts'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'title': 'foo', 'body': 'bar', 'userId': 1}),
);
print('POST request finished with status: ${postResponse.statusCode}');
print('Body preview: ${postResponse.body.substring(0, postResponse.body.length > 100 ? 100 : postResponse.body.length)}...n');
// 模拟一个失败的请求来测试 RetryInterceptor
print('--- Making a failing request (should trigger retry) ---');
final failingResponse = await client.get(Uri.parse('https://httpstat.us/503')); // 模拟一个 503 Service Unavailable
print('Failing request finished with status: ${failingResponse.statusCode}');
print('Body preview: ${failingResponse.body.substring(0, failingResponse.body.length > 100 ? 100 : failingResponse.body.length)}...n');
} catch (e) {
print('nAn unexpected error occurred: $e');
} finally {
client.close(); // 确保关闭客户端以释放资源
}
}
这个示例展示了一个完整的拦截器体系,它能够:
- 日志记录: 打印所有请求和响应的详细信息。
- 认证: 自动为请求添加认证头。
- 重试: 在遇到特定错误时自动重试请求。
- 缓存: 缓存 GET 请求的响应,提高性能。
通过这种方式,我们可以轻松地管理和扩展应用程序的网络层功能。
VII. DevTools 如何观察经过拦截的请求
现在我们已经构建了一个强大的拦截器体系,关键问题是:DevTools 网络分析器将如何看待这些经过拦截器处理的请求?它会看到原始请求,还是修改后的请求?它会记录缓存命中的请求吗?
A. 理解 DevTools 的视角:它看到的是最终的网络流量
再次强调,DevTools 网络分析器是一个被动的观察者。它通过 Dart VM Service 监听 dart:io.HttpClient 内部发出的网络事件。这意味着:
- 发送到网络的数据:DevTools 记录的是
dart:io.HttpClient实例 实际 写入 TCP/IP socket 的请求数据(包括修改后的请求头和请求体)。 - 从网络接收的数据:DevTools 记录的是
dart:io.HttpClient实例 实际 从 TCP/IP socket 读取的响应数据(包括原始响应头和响应体)。 - 应用程序内部处理:拦截器在 Dart 应用程序的内存中对请求和响应进行的修改和处理,如果这些修改最终没有改变
dart:io.HttpClient实际发送或接收的数据流,那么 DevTools 是无法直接“看到”这些中间状态的。
B. 拦截器对 DevTools 显示的影响
让我们具体分析不同拦截器对 DevTools 网络分析器显示的影响:
1. 请求Header的修改:DevTools 会显示修改后的Header
- 场景:
AuthInterceptor添加了Authorization头。 - DevTools 行为: DevTools 的请求详情面板会显示
Authorization头,因为它在dart:io.HttpClient实际发送请求时是存在的。
2. 请求Body的修改:DevTools 会显示修改后的Body
- 场景: 假设你有一个拦截器加密了请求体,或者将一个 Dart 对象序列化为不同的 JSON 格式。
- DevTools 行为: DevTools 会显示加密或序列化后的请求体内容。它不会知道原始的、未处理的 Dart 对象是什么,只会看到
dart:io.HttpClient发送的字节流。
3. 响应Header/Body的修改:DevTools 会显示 原始 响应
这是一个需要特别注意的关键点。
- 场景: 假设你的拦截器解密了响应体,或者统一了错误响应的格式。
- DevTools 行为: DevTools 记录的是
dart:io.HttpClient从网络接收到的原始响应。这意味着,如果你的拦截器在应用程序内部解密了响应体,DevTools 仍然会显示加密后的原始响应体。如果你修改了响应头或响应体内容,DevTools 看到的仍然是未修改的原始网络响应。 - 例外: 如果你的拦截器在
http.Client层返回了一个完全“伪造”的响应(例如,CacheInterceptor返回缓存),并且这个响应从未通过dart:io.HttpClient发送,那么 DevTools 就不会记录这个“请求”。
4. 重试请求:DevTools 会显示每一次重试作为独立的请求
- 场景:
RetryInterceptor在第一次请求失败后进行了重试。 - DevTools 行为: DevTools 会在网络请求列表中显示两次(或更多次,取决于重试次数)独立的网络请求。每次重试都被视为一个新的网络活动,因此会被单独记录。这对于调试重试逻辑非常有用,你可以看到每次重试的状态码和耗时。
5. 缓存请求:如果请求没有实际发出到网络,DevTools 不会记录它
- 场景:
CacheInterceptor命中了缓存,直接返回了缓存数据,而没有调用底层_inner.send(request)。 - DevTools 行为: DevTools 不会显示这个请求。因为它从未到达
dart:io.HttpClient层并被发送到网络。这是调试缓存逻辑时需要注意的,你需要通过应用程序的日志(例如LogInterceptor的输出)来确认缓存是否命中。
表格总结 DevTools 对拦截器效果的观察
| 拦截器操作类型 | DevTools 观察到的结果 | 备注 |
|---|