fix: accept buffered ACP SSE results on abrupt close

This commit is contained in:
Haitao Pan 2026-05-09 16:00:21 +08:00
parent 093db7dfc5
commit 104486a870
2 changed files with 80 additions and 10 deletions

View File

@ -1107,20 +1107,34 @@ class GatewayAcpClient {
}
}
await for (final line
in response.transform(utf8.decoder).transform(const LineSplitter())) {
if (line.isEmpty) {
if (eventLines.isNotEmpty) {
try {
await for (final line
in response.transform(utf8.decoder).transform(const LineSplitter())) {
if (line.isEmpty) {
if (eventLines.isNotEmpty) {
consumeEventPayload(eventLines.join('\n'));
eventLines.clear();
if (resolvedResponse != null) {
break;
}
}
continue;
}
if (line.startsWith('data:')) {
eventLines.add(line.substring(5).trimLeft());
}
}
} on HttpException catch (error, stackTrace) {
if (resolvedResponse == null && eventLines.isNotEmpty) {
try {
consumeEventPayload(eventLines.join('\n'));
eventLines.clear();
if (resolvedResponse != null) {
break;
}
} on FormatException {
Error.throwWithStackTrace(error, stackTrace);
}
continue;
}
if (line.startsWith('data:')) {
eventLines.add(line.substring(5).trimLeft());
if (resolvedResponse == null) {
rethrow;
}
}

View File

@ -476,6 +476,62 @@ void main() {
},
);
test(
'uses complete SSE final envelope buffered before abrupt body close',
() async {
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
addTearDown(() => server.close(force: true));
server.listen((request) async {
final body = await utf8.decoder.bind(request).join();
final envelope = jsonEncode(<String, dynamic>{
'jsonrpc': '2.0',
'id': _decodeRequestId(body),
'result': <String, dynamic>{
'output': 'stable final output',
'artifacts': <Map<String, dynamic>>[
<String, dynamic>{
'relativePath': 'exports/final.md',
'downloadUrl':
'https://xworkmate-bridge.svc.plus/artifacts/openclaw/download'
'?sessionKey=session-1&runId=run-1&relativePath=exports%2Ffinal.md',
'contentType': 'text/markdown',
'sizeBytes': 42,
},
],
},
});
final event = 'data: $envelope\n';
final eventBytes = utf8.encode(event);
request.response.headers.set(
HttpHeaders.contentTypeHeader,
'text/event-stream',
);
request.response.contentLength = eventBytes.length + 128;
final socket = await request.response.detachSocket();
socket.add(eventBytes);
await socket.flush();
socket.destroy();
});
final endpoint = Uri.parse('http://127.0.0.1:${server.port}');
final client = GatewayAcpClient(endpointResolver: () => endpoint);
final response = await client.request(
method: 'session.start',
params: const <String, dynamic>{},
);
expect((response['result'] as Map)['output'], 'stable final output');
expect(
((response['result'] as Map)['artifacts'] as List),
hasLength(1),
);
final diagnostics = (response['_xworkmateDiagnostics'] as Map)
.cast<String, dynamic>();
expect(diagnostics['transport'], 'http-sse');
expect(diagnostics['bodyRead'], isTrue);
},
);
test(
'retries interrupted TLS handshakes before surfacing ACP diagnostics',
() async {