send<T> method

Future<Response<T>> send<T>({
  1. required String method,
  2. required String path,
  3. dynamic data,
  4. Map<String, dynamic>? queryParameters,
  5. bool isLoading = false,
  6. Map<String, String>? headers,
  7. int priority = 0,
  8. bool skipDeduplication = false,
  9. bool skipQueue = false,
  10. String? baseUrl,
  11. String? service,
  12. bool isChainCall = false,
  13. void onFailure(
    1. int? httpStatusCode,
    2. int? errorCode,
    3. String message
    )?,
})

发送请求(自动处理异常,失败时自动提示) method 请求方式:必须使用 hm.get、hm.post 等常量 isLoading 是否显示加载提示(默认 false) 如果为 true 且配置了 contextGetter,将自动显示加载提示 headers 特定请求的请求头(可选),会与全局请求头合并,如果键相同则覆盖全局请求头

链式调用中的加载提示: 推荐使用 http.isLoading.send() 来明确标记链式调用,整个链路共享一个加载提示 加载提示会在整个链路结束时(成功或失败)自动关闭

请求头优先级

  1. 特定请求的 headers(最高优先级,会覆盖全局请求头)
  2. 动态请求头(dynamicHeaderBuilder)
  3. 静态请求头(staticHeaders)

示例(链式调用,推荐方式):

final result = await http.isLoading
  .send(
    method: hm.post,
    path: '/uploader/generate',
    data: {'ext': 'jpg'},
  )
  .extractModel<FileUploadResult>(FileUploadResult.fromConfigJson)
  .thenWith((uploadResult) => http.uploadToUrlResponse(...));

示例(单次请求):

final response = await http.send(
  method: hm.post,
  path: '/auth/login',
  isLoading: true, // 单次请求,请求完成后自动关闭 loading
);

示例(特定请求头):

// 某个接口需要特定的请求头
final response = await http.send(
  method: hm.post,
  path: '/special-endpoint',
  data: {'key': 'value'},
  headers: {'X-Custom-Header': 'custom-value'}, // 特定请求头,会覆盖全局同名请求头
);

示例(多服务支持):

// 配置
HttpUtil.configure(
  HttpConfig(
    baseUrl: 'https://api.example.com/v1',
    serviceBaseUrls: {
      'files': 'https://files.example.com',
      'cdn': 'https://cdn.example.com',
    },
  ),
);

// 使用默认 baseUrl
await http.send(method: hm.get, path: '/users');

// 使用服务
await http.send(method: hm.post, path: '/upload', service: 'files');

// 直接指定 baseUrl(最高优先级)
await http.send(
  method: hm.get,
  path: '/data',
  baseUrl: 'https://custom.example.com',
);

Implementation

Future<Response<T>> send<T>({
  required String method,
  required String path,
  dynamic data,
  Map<String, dynamic>? queryParameters,
  bool isLoading = false,
  Map<String, String>? headers,
  int priority = 0,
  bool skipDeduplication = false,
  bool skipQueue = false,
  String? baseUrl, // 直接指定 baseUrl(最高优先级)
  String? service, // 使用 serviceBaseUrls 中定义的服务名称
  bool isChainCall = false, // 内部参数:标记是否为链式调用(由 isLoading getter 使用)
  void Function(int? httpStatusCode, int? errorCode, String message)?
      onFailure, // 请求级别的错误处理回调
}) async {
  // 实际执行请求的函数
  Future<Response<T>> executeRequest() async {
    String? loadingId;
    bool isChainCallFlag = isChainCall; // 使用参数传入的 isChainCall

    // 如果需要显示加载提示
    if (isLoading) {
      final config = _config;
      if (config?.contextGetter != null) {
        final context = config!.contextGetter!();
        if (context != null) {
          // 检查是否已有链式调用的加载提示
          if (HttpUtil._chainLoadingId != null) {
            // 链式调用中已有加载提示,复用现有的
            loadingId = HttpUtil._chainLoadingId;
            isChainCallFlag = true;
          } else {
            // 创建新的加载提示
            loadingId = _showLoading(context, config);
            if (loadingId != null) {
              HttpUtil._chainLoadingId = loadingId;
              // 如果通过 isLoading getter 调用,isChainCall 参数为 true
              // 如果是单次请求,isChainCall 参数为 false
              isChainCallFlag = isChainCall; // 使用外部传入的 isChainCall 参数
            }
          }
        }
      }
    }

    try {
      final resolvedBaseUrl =
          HttpUtilSafeCall._resolveBaseUrl(baseUrl, service);

      final config = _config;
      if (config == null) {
        throw StateError('HttpUtil 未配置,请先调用 HttpUtil.configure() 进行配置');
      }

      Future<Response<T>> fetchAndParse() async {
        final options = dio_package.Options(
          headers: headers ?? const {},
          responseType: dio_package.ResponseType.plain,
        );
        final rawResponse = await HttpUtil.instance.request<T>(
          method: method,
          path: path,
          data: data,
          queryParameters: queryParameters,
          options: options,
          baseUrl: resolvedBaseUrl,
        );
        if (rawResponse.statusCode == 500) {
          return _handleNetworkError<T>();
        }
        dynamic responseData = rawResponse.data;
        if (responseData is String) {
          try {
            responseData =
                await compute<String, dynamic>(_decodeJsonInIsolate, responseData);
          } catch (_) {}
        }
        final raw = RawHttpResponse(
          statusCode: rawResponse.statusCode,
          data: responseData,
          path: rawResponse.requestOptions.path,
        );
        return config.responseParser.parse<T>(raw);
      }

      var response = await fetchAndParse();
      final ur = config.unauthorizedRetry;
      var unauthorizedRetryRefreshFailed = false;
      if (ur != null && !response.isSuccess) {
        for (var attempt = 0; attempt < ur.maxRetries; attempt++) {
          if (_unauthorizedRetryExcluded(path, ur)) break;
          if (!_needsUnauthorizedRetry(response, ur)) break;
          final ok = await ur.refreshAccessToken();
          if (!ok) {
            await ur.onRefreshFailed?.call();
            unauthorizedRetryRefreshFailed = true;
            break;
          }
          response = await fetchAndParse();
          if (response.isSuccess) break;
        }
      }

      if (!response.isSuccess) {
        response.handleError();

        // 延迟调用全局的错误处理回调,确保链式调用的 onFailure 可以先执行
        // 优先级:链式调用的 onFailure > on401Unauthorized > 全局的 onFailure
        final errorMessage = response.errorMessage;
        if (errorMessage != null) {
          final httpStatusCode = response.httpStatusCode;
          final errorCode = response.errorCode;
          final suppressLegacyAuthCallbacks = unauthorizedRetryRefreshFailed &&
              ur != null &&
              _isUnauthorizedResponseShape(response, ur);
          Future.microtask(() {
            if (!response.errorHandled) {
              if (onFailure != null) {
                onFailure(httpStatusCode, errorCode, errorMessage);
                response.onFailure((_, __, ___) {});
                return;
              }

              if (suppressLegacyAuthCallbacks) {
                return;
              }

              if (httpStatusCode == 401 && config.on401Unauthorized != null) {
                if (HttpUtilSafeCall._shouldHandleError(
                    401, config.errorDeduplicationWindow)) {
                  config.on401Unauthorized!();
                }
                return;
              }

              if (config.onFailure != null) {
                config.onFailure!(httpStatusCode, errorCode, errorMessage);
              }
            }
          });
        }
      }

      return response;
    } catch (e) {
      // 所有异常都统一处理为网络错误
      return _handleNetworkError<T>();
    } finally {
      // 如果不是链式调用(通过 http.isLoading.send() 标记),延迟关闭加载提示并清理 _chainLoadingId
      // 使用 Future.microtask 确保在扩展方法(如 extractField、onSuccess、extractModel)之后执行
      // 如果是链式调用(isChainCallFlag = true),加载提示会在整个链路结束时关闭(由 thenWithUpdate 等方法关闭)
      // 注意:extractModel、extractField 等方法不再关闭 loading,统一由 finally 块或链式调用的最后一步关闭
      if (isLoading && loadingId != null && !isChainCallFlag) {
        Future.microtask(() {
          // 再次检查 loadingId 是否仍然存在,并且确保不是链式调用
          // 如果用户只使用了 await http.send(isLoading: true),这里会关闭 loading
          // 如果用户使用了 extractModel 等方法但没有后续链式调用,这里也会关闭 loading
          // 如果用户使用了 thenWith 等链式调用,loading 会由链式调用的最后一步关闭
          // 关键:如果 _chainLoadingId 仍然存在且等于当前 loadingId,说明没有后续链式调用,可以关闭
          // 如果 _chainLoadingId 已经被后续链式调用修改或清空,说明有链式调用,不应该关闭
          if (HttpUtil._chainLoadingId == loadingId) {
            _hideLoading(loadingId);
            HttpUtil._chainLoadingId = null;
          }
        });
      }
    }
  }

  // 如果启用了队列且未跳过队列,加入队列
  if (HttpUtil._requestQueue != null && !skipQueue) {
    return HttpUtil._requestQueue!.enqueue<Response<T>>(
      priority: priority,
      requestExecutor: () {
        // 如果启用了去重且未跳过去重,使用去重管理器
        if (HttpUtil._deduplicator != null && !skipDeduplication) {
          // 解析 baseUrl 用于去重
          final resolvedBaseUrl =
              HttpUtilSafeCall._resolveBaseUrl(baseUrl, service);
          return HttpUtil._deduplicator!.execute<Response<T>>(
            method: method,
            path: path,
            queryParameters: queryParameters,
            data: data,
            baseUrl: resolvedBaseUrl,
            requestExecutor: executeRequest,
          );
        } else {
          return executeRequest();
        }
      },
    );
  }

  // 如果启用了去重且未跳过去重,使用去重管理器
  if (HttpUtil._deduplicator != null && !skipDeduplication) {
    // 解析 baseUrl 用于去重
    final resolvedBaseUrl =
        HttpUtilSafeCall._resolveBaseUrl(baseUrl, service);
    return HttpUtil._deduplicator!.execute<Response<T>>(
      method: method,
      path: path,
      queryParameters: queryParameters,
      data: data,
      baseUrl: resolvedBaseUrl,
      requestExecutor: executeRequest,
    );
  }

  // 直接执行请求
  return executeRequest();
}