diff --git a/src/vs/platform/agentHost/common/state/sessionProtocol.ts b/src/vs/platform/agentHost/common/state/sessionProtocol.ts index ebbad8027ad..9a2380278b2 100644 --- a/src/vs/platform/agentHost/common/state/sessionProtocol.ts +++ b/src/vs/platform/agentHost/common/state/sessionProtocol.ts @@ -76,6 +76,7 @@ export function isJsonRpcResponse(msg: IProtocolMessage): msg is IJsonRpcRespons // ---- JSON-RPC error codes --------------------------------------------------- +export const JSON_RPC_PARSE_ERROR = -32700; export const JSON_RPC_INTERNAL_ERROR = -32603; // ---- Shared data types ------------------------------------------------------ diff --git a/src/vs/platform/agentHost/node/webSocketTransport.ts b/src/vs/platform/agentHost/node/webSocketTransport.ts index 8c53abdab01..3301d87c568 100644 --- a/src/vs/platform/agentHost/node/webSocketTransport.ts +++ b/src/vs/platform/agentHost/node/webSocketTransport.ts @@ -10,7 +10,7 @@ import { Emitter } from '../../../base/common/event.js'; import { Disposable } from '../../../base/common/lifecycle.js'; import { connectionTokenQueryName } from '../../../base/common/network.js'; import { ILogService } from '../../log/common/log.js'; -import type { IProtocolMessage } from '../common/state/sessionProtocol.js'; +import { JSON_RPC_PARSE_ERROR, type IProtocolMessage } from '../common/state/sessionProtocol.js'; import type { IProtocolServer, IProtocolTransport } from '../common/state/sessionTransport.js'; import { protocolReplacer, protocolReviver } from '../common/state/jsonSerialization.js'; @@ -58,7 +58,7 @@ export class WebSocketProtocolTransport extends Disposable implements IProtocolT const message = JSON.parse(text, protocolReviver) as IProtocolMessage; this._onMessage.fire(message); } catch { - // Malformed message — drop. No logger available at transport level. + this.send({ jsonrpc: '2.0', id: null!, error: { code: JSON_RPC_PARSE_ERROR, message: 'Parse error' } }); } }); diff --git a/src/vs/platform/agentHost/test/node/protocolWebSocket.integrationTest.ts b/src/vs/platform/agentHost/test/node/protocolWebSocket.integrationTest.ts index 0f411e14a7e..367a0369f1a 100644 --- a/src/vs/platform/agentHost/test/node/protocolWebSocket.integrationTest.ts +++ b/src/vs/platform/agentHost/test/node/protocolWebSocket.integrationTest.ts @@ -13,6 +13,7 @@ import { protocolReplacer, protocolReviver } from '../../common/state/jsonSerial import { isJsonRpcNotification, isJsonRpcResponse, + JSON_RPC_PARSE_ERROR, type IActionBroadcastParams, type IFetchTurnsResult, type IJsonRpcErrorResponse, @@ -140,6 +141,31 @@ class TestProtocolClient { return predicate ? this._notifications.filter(predicate) : [...this._notifications]; } + /** Send a raw string over the WebSocket without JSON serialization. */ + sendRaw(data: string): void { + this._ws.send(data); + } + + /** Wait for the next raw message from the server. */ + waitForRawMessage(timeoutMs = 5000): Promise { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + cleanup(); + reject(new Error(`Timeout waiting for raw message (${timeoutMs}ms)`)); + }, timeoutMs); + const onMsg = (data: Buffer | string) => { + cleanup(); + const text = typeof data === 'string' ? data : data.toString('utf-8'); + resolve(JSON.parse(text)); + }; + const cleanup = () => { + clearTimeout(timer); + this._ws.removeListener('message', onMsg); + }; + this._ws.on('message', onMsg); + }); + } + close(): void { for (const w of this._notifWaiters) { w.reject(new Error('Client closed')); @@ -636,4 +662,21 @@ suite('Protocol WebSocket E2E', function () { const state = snapshot.state as ISessionState; assert.strictEqual(state.summary.model, 'new-mock-model'); }); + + test('malformed JSON message returns parse error', async function () { + this.timeout(10_000); + + const raw = new TestProtocolClient(server.port); + await raw.connect(); + + const responsePromise = raw.waitForRawMessage(); + raw.sendRaw('this is not valid json{{{'); + + const response = await responsePromise as IJsonRpcErrorResponse; + assert.strictEqual(response.jsonrpc, '2.0'); + assert.strictEqual(response.id, null); + assert.strictEqual(response.error.code, JSON_RPC_PARSE_ERROR); + + raw.close(); + }); });