|
3 | 3 | import { ConfigService } from '@hashgraph/json-rpc-config-service/dist/services'; |
4 | 4 | import { ParameterizedContext } from 'koa'; |
5 | 5 |
|
6 | | -interface JsonRpcSuccess { |
7 | | - jsonrpc: unknown; |
8 | | - id: string | number | unknown; |
9 | | - result: unknown; |
10 | | -} |
11 | | - |
12 | | -interface JsonRpcError { |
13 | | - jsonrpc: unknown; |
14 | | - id: string | number | unknown; |
15 | | - error?: { |
16 | | - code?: number; |
17 | | - message: unknown; |
18 | | - data?: unknown; |
19 | | - }; |
20 | | -} |
21 | | - |
22 | | -type JsonRpcResponse = JsonRpcSuccess | JsonRpcError; |
23 | | -type JsonRpcBatch = JsonRpcResponse[]; |
24 | | -type JsonRpcBody = JsonRpcResponse | JsonRpcBatch; |
25 | | - |
26 | 6 | const VALID_JSON_RPC_HTTP_REQUESTS_STATUS_CODE = ConfigService.get('VALID_JSON_RPC_HTTP_REQUESTS_STATUS_CODE'); |
27 | 7 |
|
28 | | -const isCorrectSuccess = (res: JsonRpcResponse, httpStatus: unknown) => |
29 | | - httpStatus === 200 && |
30 | | - res.jsonrpc === '2.0' && |
31 | | - hasValidId(res.id) && |
32 | | - Object.prototype.hasOwnProperty.call(res, 'result'); |
33 | | - |
34 | | -const hasValidId = (id: unknown) => Boolean(id !== undefined && id !== null); |
35 | | - |
36 | | -const isCorrectError = (res: JsonRpcResponse, httpStatus: unknown) => |
37 | | - httpStatus !== 200 && |
38 | | - res.jsonrpc === '2.0' && |
39 | | - hasValidId(res.id) && |
40 | | - Object.prototype.hasOwnProperty.call(res, 'error') && |
41 | | - typeof (res as JsonRpcError).error?.message === 'string'; |
42 | | - |
43 | | -const hasCorrectResponseBody = (res: JsonRpcResponse, httpStatus: unknown) => |
44 | | - isCorrectSuccess(res, httpStatus) || isCorrectError(res, httpStatus); |
45 | | - |
46 | | -const fixResponseBody = (res: Partial<JsonRpcResponse> | undefined, httpStatus: unknown) => { |
47 | | - const id = hasValidId(res?.id) ? (res!.id as string | number) : null; |
48 | | - if (httpStatus === 200) { |
49 | | - return { |
50 | | - jsonrpc: '2.0', |
51 | | - id, |
52 | | - result: Object.prototype.hasOwnProperty.call(res ?? {}, 'result') ? (res as JsonRpcSuccess).result : '0x', |
53 | | - }; |
54 | | - } |
55 | | - const errorObj = |
56 | | - 'error' in (res ?? {}) && (res as JsonRpcError).error |
57 | | - ? { |
58 | | - code: (res as JsonRpcError).error!.code ?? -32603, |
59 | | - message: (res as JsonRpcError).error!.message, |
60 | | - } |
61 | | - : { |
62 | | - code: -32603, |
63 | | - message: 'Internal error', |
64 | | - }; |
| 8 | +const FALLBACK_RESPONSE_BODY = { |
| 9 | + jsonrpc: '2.0', |
| 10 | + id: null, |
| 11 | + error: { code: -32600, message: 'Request body is empty; expected a JSON-RPC 2.0 request' }, |
| 12 | +}; |
65 | 13 |
|
66 | | - return { |
67 | | - jsonrpc: '2.0', |
68 | | - id, |
69 | | - error: errorObj, |
70 | | - }; |
| 14 | +export const INVALID_METHOD_RESPONSE_BODY = { |
| 15 | + ...FALLBACK_RESPONSE_BODY, |
| 16 | + error: { code: -32600, message: 'Invalid HTTP method: only POST is allowed' }, |
71 | 17 | }; |
72 | 18 |
|
73 | 19 | /** |
74 | 20 | * Ensures a JSON-RPC response uses a valid JSON-RPC 2.0 structure. |
75 | 21 | * Normalizes missing or invalid fields for both single and batch responses. |
76 | 22 | * May update HTTP status depending on VALID_JSON_RPC_HTTP_REQUESTS_STATUS_CODE. |
77 | 23 | * |
78 | | - * This function must be invoked in any place where Koa's `next()` might not run, |
79 | | - * or where the response may bypass normal middleware cleanup - so it's not implemented as a KOA's middleware. |
80 | | - * |
81 | 24 | * @param {ParameterizedContext} ctx - Koa context containing status and body. |
82 | 25 | */ |
83 | | -export const jsonRpcComplianceLayer = async (ctx: ParameterizedContext) => { |
84 | | - const body = ctx.body as JsonRpcBody | undefined; |
85 | | - if (!body) { |
86 | | - ctx.body = fixResponseBody(undefined, ctx.status); |
87 | | - ctx.status = VALID_JSON_RPC_HTTP_REQUESTS_STATUS_CODE ? 200 : 400; |
| 26 | +export const jsonRpcComplianceLayer = async ( |
| 27 | + ctx: { |
| 28 | + body: { |
| 29 | + jsonrpc: unknown; |
| 30 | + id: unknown; |
| 31 | + result?: unknown; |
| 32 | + error?: { code: unknown; message: unknown }; |
| 33 | + }; |
| 34 | + status: number | undefined; |
| 35 | + } & ParameterizedContext, |
| 36 | +) => { |
| 37 | + if (!ctx.body) { |
| 38 | + ctx.status = 400; |
| 39 | + ctx.body = FALLBACK_RESPONSE_BODY; |
88 | 40 | return; |
89 | 41 | } |
90 | | - if (Array.isArray(body)) { |
91 | | - // Regardless of the mode the batch requests will always return 200; |
| 42 | + |
| 43 | + // Always return 200 for array requests. |
| 44 | + if (Array.isArray(ctx.body)) { |
92 | 45 | ctx.status = 200; |
93 | 46 | return; |
94 | 47 | } |
| 48 | + if (typeof ctx.body !== 'object') { |
| 49 | + ctx.status = 400; |
| 50 | + ctx.body = FALLBACK_RESPONSE_BODY; |
| 51 | + return; |
| 52 | + } |
95 | 53 |
|
96 | | - if (!hasCorrectResponseBody(body, ctx.status)) ctx.body = fixResponseBody(body, ctx.status); |
| 54 | + if (!ctx.body.jsonrpc) ctx.body.jsonrpc = FALLBACK_RESPONSE_BODY.jsonrpc; |
| 55 | + if (!ctx.body.id) ctx.body.id = FALLBACK_RESPONSE_BODY.id; |
| 56 | + if (ctx.status === 200 && !ctx.body.result) ctx.body.result = '0x'; |
| 57 | + if (ctx.status === 400 && (!ctx.body.error || !ctx.body.error.code)) ctx.body.error = FALLBACK_RESPONSE_BODY.error; |
97 | 58 | if (ctx.status === 400 && VALID_JSON_RPC_HTTP_REQUESTS_STATUS_CODE) ctx.status = 200; |
98 | 59 | }; |
0 commit comments