Merge branch 'codex/acp-jsonfix'
This commit is contained in:
commit
fa068bc815
@ -465,6 +465,21 @@ class GatewayAcpClient {
|
||||
.value(HttpHeaders.contentTypeHeader)
|
||||
?.toLowerCase() ??
|
||||
'';
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
final body = await response.transform(utf8.decoder).join();
|
||||
throw GatewayAcpException(
|
||||
_describeHttpError(
|
||||
statusCode: response.statusCode,
|
||||
contentType: contentType,
|
||||
body: body,
|
||||
),
|
||||
code: 'ACP_HTTP_${response.statusCode}',
|
||||
details: <String, dynamic>{
|
||||
'statusCode': response.statusCode,
|
||||
'contentType': contentType,
|
||||
},
|
||||
);
|
||||
}
|
||||
if (contentType.contains('text/event-stream')) {
|
||||
return _consumeSseRpcResponse(
|
||||
response: response,
|
||||
@ -481,6 +496,58 @@ class GatewayAcpClient {
|
||||
}
|
||||
}
|
||||
|
||||
String _describeHttpError({
|
||||
required int statusCode,
|
||||
required String contentType,
|
||||
required String body,
|
||||
}) {
|
||||
final base = 'ACP HTTP request failed ($statusCode)';
|
||||
final normalizedType = contentType.trim();
|
||||
if (normalizedType.isNotEmpty &&
|
||||
!_contentTypeLooksJsonOrSse(normalizedType)) {
|
||||
return '$base · unexpected content type: $normalizedType';
|
||||
}
|
||||
|
||||
final detail = _extractErrorDetail(body);
|
||||
if (detail.isNotEmpty) {
|
||||
return '$base · $detail';
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
bool _contentTypeLooksJsonOrSse(String contentType) {
|
||||
return contentType.contains('application/json') ||
|
||||
contentType.contains('application/problem+json') ||
|
||||
contentType.contains('text/json') ||
|
||||
contentType.contains('text/event-stream');
|
||||
}
|
||||
|
||||
String _extractErrorDetail(String body) {
|
||||
final trimmed = body.trim();
|
||||
if (trimmed.isEmpty) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
final decoded = _decodeMap(trimmed);
|
||||
final error = asMap(decoded['error']);
|
||||
return (stringValue(error['message']) ??
|
||||
stringValue(decoded['message']) ??
|
||||
stringValue(decoded['detail']) ??
|
||||
'')
|
||||
.trim();
|
||||
} on FormatException {
|
||||
// Fall through to textual snippet extraction below.
|
||||
}
|
||||
|
||||
final singleLine = trimmed.replaceAll(RegExp(r'\s+'), ' ');
|
||||
if (singleLine.isEmpty) {
|
||||
return '';
|
||||
}
|
||||
return singleLine.length <= 160
|
||||
? singleLine
|
||||
: '${singleLine.substring(0, 157)}...';
|
||||
}
|
||||
|
||||
Future<String> _resolveAuthorizationHeader(
|
||||
Uri endpoint, {
|
||||
String authorizationOverride = '',
|
||||
|
||||
@ -43,6 +43,29 @@ void main() {
|
||||
expect(server.rpcMethods, contains('acp.capabilities'));
|
||||
});
|
||||
|
||||
test('surfaces HTTP content-type errors without raw JSON parse failures', () async {
|
||||
final server = await _AcpFakeServer.start(
|
||||
disableWebSocket: true,
|
||||
respondWithHtmlError: true,
|
||||
);
|
||||
addTearDown(server.close);
|
||||
|
||||
final client = GatewayAcpClient(
|
||||
endpointResolver: () => server.baseHttpUri,
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
() => client.loadCapabilities(forceRefresh: true),
|
||||
throwsA(
|
||||
isA<GatewayAcpException>().having(
|
||||
(error) => error.toString(),
|
||||
'message',
|
||||
contains('unexpected content type: text/html'),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'forwards ACP authorization resolver headers over websocket',
|
||||
() async {
|
||||
@ -128,19 +151,31 @@ void main() {
|
||||
}
|
||||
|
||||
class _AcpFakeServer {
|
||||
_AcpFakeServer._(this._server, {required this.disableWebSocket});
|
||||
_AcpFakeServer._(
|
||||
this._server, {
|
||||
required this.disableWebSocket,
|
||||
required this.respondWithHtmlError,
|
||||
});
|
||||
|
||||
final HttpServer _server;
|
||||
final bool disableWebSocket;
|
||||
final bool respondWithHtmlError;
|
||||
final List<String> rpcMethods = <String>[];
|
||||
String? lastWebSocketAuthorization;
|
||||
String? lastHttpAuthorization;
|
||||
|
||||
Uri get baseHttpUri => Uri.parse('http://127.0.0.1:${_server.port}');
|
||||
|
||||
static Future<_AcpFakeServer> start({bool disableWebSocket = false}) async {
|
||||
static Future<_AcpFakeServer> start({
|
||||
bool disableWebSocket = false,
|
||||
bool respondWithHtmlError = false,
|
||||
}) async {
|
||||
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
||||
final fake = _AcpFakeServer._(server, disableWebSocket: disableWebSocket);
|
||||
final fake = _AcpFakeServer._(
|
||||
server,
|
||||
disableWebSocket: disableWebSocket,
|
||||
respondWithHtmlError: respondWithHtmlError,
|
||||
);
|
||||
unawaited(fake._listen());
|
||||
return fake;
|
||||
}
|
||||
@ -200,6 +235,15 @@ class _AcpFakeServer {
|
||||
lastHttpAuthorization = request.headers.value(
|
||||
HttpHeaders.authorizationHeader,
|
||||
);
|
||||
if (respondWithHtmlError) {
|
||||
request.response.statusCode = HttpStatus.notFound;
|
||||
request.response.headers.set(HttpHeaders.contentTypeHeader, 'text/html');
|
||||
request.response.write(
|
||||
'<!doctype html><script>var key = "opencode-theme-id"</script>',
|
||||
);
|
||||
await request.response.close();
|
||||
return;
|
||||
}
|
||||
final body = await utf8.decodeStream(request);
|
||||
final envelope = _decodeMap(body);
|
||||
final id = envelope['id'];
|
||||
|
||||
Loading…
Reference in New Issue
Block a user