DevTools Network Profiler:拦截 Dart `HttpClient` 请求的实现细节

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 请求生命周期如下:

  1. 创建客户端: HttpClient client = HttpClient();
  2. 创建请求: HttpClientRequest request = await client.getUrl(uri);
  3. 配置请求: 设置请求头、请求体。
  4. 发送请求: HttpClientResponse response = await request.close(); (这会建立连接、发送请求头和体)
  5. 处理响应: 读取响应状态、响应头、响应体。
  6. 关闭客户端: 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.ResponseString/List<int>

IOClienthttp.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. BaseClientwith 模式

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,减少了底层 StreamFuture 的直接操作。
  • 灵活性: 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;
}

理解了 HttpClientpackage: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:httpIOClient(或任何基于 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();
  }
}

这种底层包装的缺点:

  • 繁琐: 需要手动包装 HttpClientHttpClientRequest/HttpClientResponse 的所有方法和属性,工作量巨大且容易出错。
  • 复杂的状态管理: 尤其是在处理 HttpClientResponseStream 时,需要确保响应体只被读取一次,或者正确地缓存和重放。
  • 不兼容 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 方法中调用被包装的 Clientsend 方法,从而形成一个处理链。

3. 拦截器链的构建与管理
  • 顺序与依赖: 拦截器的顺序非常重要。例如,认证拦截器应该在日志拦截器之前添加认证头,这样日志拦截器才能记录带认证头的请求。重试拦截器应该在认证拦截器之后,以便在重试时使用正确的认证信息。

    • 请求流向: Client A -> Client B -> Client C -> IOClient (实际网络请求)
    • 响应流向: IOClient -> Client C -> Client B -> Client A

    在上面的 createClientWithInterceptors() 函数中,我们按照“最外层处理最通用或最先发生”的原则来组织:

    1. RetryClient(client): 最先处理可能的网络错误和重试。
    2. AuthClient(client, ...): 接着添加认证信息。
    3. 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 和一个 HttpHandlerhandler 代表了链中后续的所有处理步骤,包括最终发送请求的 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();
  }
}

InterceptedClientsend 方法是核心:它通过循环构建了一个 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.BaseRequestfinalize() 方法会消耗请求体流,http.StreamedResponsestream 也会消耗响应体流,因此为了能在日志中打印请求/响应体并同时将它们传递给下一个处理器或返回给调用者,我们必须:

  1. 对于请求体: 在 handler 调用之前,将 http.BaseRequest 的体流完全读取出来(例如通过 request.finalize().toBytes()),然后创建一个新的 http.StreamedRequest,并将读取到的体数据重新写入新请求的 sink
  2. 对于响应体: 在 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 观察到的结果 备注

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注