Files
proxypin/lib/network/components/report_server_interceptor.dart
Pandaver b887aa4854 fix: handle error phase and sync response param in split report mode
分离模式下 onError 时跳过 response 阶段上报,
同时显式同步 response 参数到 request.response 避免参数被忽略。
2026-04-22 21:24:06 +08:00

152 lines
4.9 KiB
Dart

/*
* Copyright 2024 Hongen Wang All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:proxypin/network/util/compress.dart';
import 'package:proxypin/network/util/logger.dart';
import 'package:proxypin/utils/har.dart';
import '../http/http.dart';
import 'interceptor.dart';
import 'manager/report_server_manager.dart';
/// Hosts interceptor
/// @author wanghongen
class ReportServerInterceptor extends Interceptor {
Future<ReportServerManager> get reportServerManager async => await ReportServerManager.instance;
static HttpClient httpClient = HttpClient();
@override
int get priority => 1000;
@override
Future<HttpRequest?> onRequest(HttpRequest request) async {
unawaited(_reportRequestIfSplit(request));
return request;
}
@override
Future<HttpResponse?> onResponse(HttpRequest request, HttpResponse response) async {
unawaited(reportServer(request, response));
return response;
}
@override
Future<void> onError(HttpRequest? request, error, StackTrace? stackTrace) async {
if (request != null) {
unawaited(reportServer(request, null, error: error, stackTrace: stackTrace));
}
return;
}
Future<void> _reportRequestIfSplit(HttpRequest request) async {
String requestUrl = request.requestUrl;
var manager = await reportServerManager;
var server = await manager.matchServer(requestUrl);
if (server == null || !server.splitReport) {
return;
}
var payload = Har.toHarRequest(request);
await _sendReport(server, payload, requestUrl, phase: "request");
}
Future<void> reportServer(HttpRequest request, HttpResponse? response,
{dynamic error, StackTrace? stackTrace}) async {
if (response != null) {
request.response = response;
}
String requestUrl = request.requestUrl;
var manager = await reportServerManager;
var server = await manager.matchServer(requestUrl);
if (server == null) {
return;
}
Map payload;
String? phase;
if (server.splitReport) {
if (request.response == null) {
return;
}
payload = Har.toHarResponse(request);
phase = "response";
} else {
payload = Har.toHar(request);
}
await _sendReport(server, payload, requestUrl, phase: phase);
}
Future<void> _sendReport(ReportServer server, Map payload, String requestUrl, {String? phase}) async {
try {
logger.i("reportServer start: $requestUrl -> ${server.name} (${server.serverUrl})");
var serverUrl = (server.serverUrl).trim();
if (serverUrl.isEmpty) {
logger.w('reportServer skipped: serverUrl empty for ${server.name}');
return;
}
if (!serverUrl.startsWith('http://') && !serverUrl.startsWith('https://')) {
serverUrl = 'http://$serverUrl';
}
final uri = Uri.parse(serverUrl);
List<int> body = utf8.encode(jsonEncode(payload));
final compression = server.compression?.toLowerCase();
if (compression == 'gzip') {
try {
body = gzipEncode(body);
} catch (e) {
logger.w('reportServer gzip compress failed: $e');
}
}
final ioReq = await httpClient.postUrl(uri).timeout(const Duration(seconds: 5));
final matchedRule = server.name;
if (matchedRule.isNotEmpty) {
// URL encode the server name to support non-ASCII characters (e.g., Chinese)
final encodedName = Uri.encodeComponent(matchedRule);
ioReq.headers.set('X-Report-Name', encodedName);
}
if (phase != null) {
ioReq.headers.set('X-Report-Phase', phase);
}
ioReq.headers.set(HttpHeaders.contentTypeHeader, 'application/json; charset=utf-8');
if (compression == 'gzip') {
ioReq.headers.set(HttpHeaders.contentEncodingHeader, 'gzip');
}
ioReq.add(body);
final ioResp = await ioReq.close().timeout(const Duration(seconds: 30));
final respText = await ioResp.transform(utf8.decoder).join();
if (ioResp.statusCode >= 200 && ioResp.statusCode < 300) {
logger.i('reportServer delivered to ${server.name} (${uri.toString()}), status=${ioResp.statusCode}');
} else {
logger.w('reportServer delivery to ${server.name} failed, status=${ioResp.statusCode}, body=$respText');
}
} catch (e, st) {
logger.e("reportServer error $requestUrl", error: e, stackTrace: st);
}
}
}