fix: accept buffered ACP SSE results on abrupt close
This commit is contained in:
parent
093db7dfc5
commit
104486a870
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user