Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/validate-json-rpc-params.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@aws-blocks/core": patch
---

Reject primitive and null JSON-RPC params with the standard Invalid Params error.
63 changes: 63 additions & 0 deletions packages/core/src/rpc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,66 @@ describe('-32600 Invalid Request error shape', () => {
}
});
});

describe('-32602 Invalid Params validation', () => {
it('accepts positional array params', () => {
const result = parseRpcRequest(JSON.stringify({
jsonrpc: '2.0',
method: 'api.echo',
params: ['hello', 42],
id: 1,
}));

assert.strictEqual(result.ok, true);
if (result.ok) {
assert.deepStrictEqual(result.request.args, ['hello', 42]);
}
});

it('accepts named object params', () => {
const result = parseRpcRequest(JSON.stringify({
jsonrpc: '2.0',
method: 'api.echo',
params: { message: 'hello', count: 42 },
id: 2,
}));

assert.strictEqual(result.ok, true);
if (result.ok) {
assert.deepStrictEqual(result.request.args, ['hello', 42]);
}
});

it('treats omitted params as no arguments', () => {
const result = parseRpcRequest(JSON.stringify({
jsonrpc: '2.0',
method: 'api.ping',
id: 3,
}));

assert.strictEqual(result.ok, true);
if (result.ok) {
assert.deepStrictEqual(result.request.args, []);
}
});

for (const params of ['abc', 42, true, false, null]) {
it(`rejects ${JSON.stringify(params)} params`, () => {
const result = parseRpcRequest(JSON.stringify({
jsonrpc: '2.0',
method: 'api.echo',
params,
id: 'request-1',
}));

assert.strictEqual(result.ok, false);
if (!result.ok) {
const response = JSON.parse(result.response);
assert.strictEqual(response.error.code, RpcErrorCode.InvalidParams);
assert.strictEqual(response.error.data.name, 'InvalidParams');
assert.ok(response.error.message.includes('expected an array or object'));
assert.strictEqual(response.id, 'request-1');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests assert error.code and error.data.name but not error.message. The existing -32600 tests assert message content; the user-facing -32602 message could regress silently. One assertion would close the gap:

assert.ok(response.error.message.includes('expected an array or object'));

}
});
}
});
24 changes: 20 additions & 4 deletions packages/core/src/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,15 +118,31 @@ export function parseRpcRequest(bodyText: string): RpcParseResult {
};
}

const hasParams = Object.hasOwn(parsed, 'params');
if (hasParams && (parsed.params === null || typeof parsed.params !== 'object')) {
return {
ok: false,
response: errorResponse(
RpcErrorCode.InvalidParams,
'Invalid params: expected an array or object',
id,
{ name: 'InvalidParams' },
),
};
}

let args: unknown[] = [];
if (hasParams) {
args = Array.isArray(parsed.params) ? parsed.params : Object.values(parsed.params);
}

return {
ok: true,
request: {
apiNamespace: parsed.method.substring(0, dotIndex),
method: parsed.method.substring(dotIndex + 1),
// JSON-RPC 2.0 §4.2: params may be an array (positional) or object (named).
args: Array.isArray(parsed.params)
? parsed.params
: Object.values(parsed.params ?? {}),
// JSON-RPC 2.0 §4.2: params may be array (positional), object (named), or omitted (empty).
args,
id,
},
};
Expand Down
Loading