Skip to content

Commit 25b5953

Browse files
committed
feat: status handling (#4630)
Signed-off-by: Mariusz Jasuwienas <[email protected]>
1 parent 2e9648a commit 25b5953

File tree

6 files changed

+623
-20
lines changed

6 files changed

+623
-20
lines changed

packages/config-service/src/services/globalConfig.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,11 @@ const _CONFIG = {
739739
required: false,
740740
defaultValue: 10,
741741
},
742+
VALID_JSON_RPC_HTTP_REQUESTS_STATUS_CODE: {
743+
type: 'boolean',
744+
required: false,
745+
defaultValue: false,
746+
},
742747
} as const satisfies { [key: string]: ConfigProperty }; // Ensures _CONFIG is read-only and conforms to the ConfigProperty structure
743748

744749
export type ConfigKey = keyof typeof _CONFIG;

packages/server/src/compliance.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
import { ConfigService } from '@hashgraph/json-rpc-config-service/dist/services';
4+
import { ParameterizedContext } from 'koa';
5+
6+
interface IResponseContext {
7+
body: {
8+
jsonrpc: unknown;
9+
id: unknown;
10+
result?: unknown;
11+
error?: { code: unknown; message: unknown };
12+
};
13+
status: number | undefined;
14+
}
15+
16+
const VALID_JSON_RPC_HTTP_REQUESTS_STATUS_CODE = ConfigService.get('VALID_JSON_RPC_HTTP_REQUESTS_STATUS_CODE');
17+
18+
const FALLBACK_RESPONSE_BODY = {
19+
jsonrpc: '2.0',
20+
id: null,
21+
error: { code: -32600, message: 'Request body is empty; expected a JSON-RPC 2.0 request' },
22+
};
23+
24+
export const INVALID_METHOD_RESPONSE_BODY = {
25+
...FALLBACK_RESPONSE_BODY,
26+
error: { code: -32600, message: 'Invalid HTTP method: only POST is allowed' },
27+
};
28+
29+
const makeSureBodyExistsAndCanBeChecked = (ctx: IResponseContext) => {
30+
if (!ctx.body) {
31+
ctx.status = 400;
32+
ctx.body = FALLBACK_RESPONSE_BODY;
33+
return false;
34+
}
35+
36+
if (Array.isArray(ctx.body)) {
37+
ctx.status = 200;
38+
return false;
39+
}
40+
41+
if (typeof ctx.body !== 'object') {
42+
ctx.status = 400;
43+
ctx.body = FALLBACK_RESPONSE_BODY;
44+
return false;
45+
}
46+
ctx.body.jsonrpc ||= FALLBACK_RESPONSE_BODY.jsonrpc;
47+
ctx.body.id ||= FALLBACK_RESPONSE_BODY.id;
48+
49+
return true;
50+
};
51+
52+
/**
53+
* Ensures a JSON-RPC response uses a valid JSON-RPC 2.0 structure.
54+
* Normalizes missing or invalid fields for both single and batch responses.
55+
* May update HTTP status depending on VALID_JSON_RPC_HTTP_REQUESTS_STATUS_CODE.
56+
*
57+
* @param {IResponseContext & ParameterizedContext} ctx - Koa context containing status and body.
58+
*/
59+
export const jsonRpcComplianceLayer = async (ctx: IResponseContext & ParameterizedContext) => {
60+
if (!makeSureBodyExistsAndCanBeChecked(ctx)) return;
61+
if (ctx.status === 400) {
62+
if (!ctx.body.error || !ctx.body.error.code) ctx.body.error = FALLBACK_RESPONSE_BODY.error;
63+
if (VALID_JSON_RPC_HTTP_REQUESTS_STATUS_CODE) ctx.status = 200;
64+
}
65+
};

packages/server/src/koaJsonRpc/index.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,13 @@ export default class KoaJsonRpc {
124124

125125
// verify max batch size
126126
if (body.length > this.batchRequestsMaxSize) {
127-
ctx.body = jsonRespError(
128-
null,
129-
predefined.BATCH_REQUESTS_AMOUNT_MAX_EXCEEDED(body.length, this.batchRequestsMaxSize),
130-
requestId,
131-
);
127+
ctx.body = [
128+
jsonRespError(
129+
null,
130+
predefined.BATCH_REQUESTS_AMOUNT_MAX_EXCEEDED(body.length, this.batchRequestsMaxSize),
131+
requestId,
132+
),
133+
];
132134
ctx.status = 400;
133135
ctx.state.status = `${ctx.status} (${INVALID_REQUEST})`;
134136
return;

packages/server/src/server.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import pino from 'pino';
1212
import { collectDefaultMetrics, Histogram, Registry } from 'prom-client';
1313
import { v4 as uuid } from 'uuid';
1414

15+
import { INVALID_METHOD_RESPONSE_BODY, jsonRpcComplianceLayer } from './compliance';
1516
import { formatRequestIdMessage } from './formatters';
1617
import KoaJsonRpc from './koaJsonRpc';
1718
import { spec } from './koaJsonRpc/lib/RpcError';
@@ -277,7 +278,8 @@ export async function initializeServer() {
277278
// support CORS preflight
278279
ctx.status = 200;
279280
} else {
280-
logger.warn(`skipping HTTP method: [${ctx.method}], url: ${ctx.url}, status: ${ctx.status}`);
281+
ctx.status = 403;
282+
ctx.body = INVALID_METHOD_RESPONSE_BODY;
281283
}
282284
});
283285

@@ -307,10 +309,13 @@ export async function initializeServer() {
307309

308310
const rpcApp = koaJsonRpc.rpcApp();
309311

310-
app.use(async (ctx) => {
312+
app.use(async (ctx, next) => {
311313
await rpcApp(ctx);
314+
await next();
312315
});
313316

317+
app.use(jsonRpcComplianceLayer);
318+
314319
process.on('unhandledRejection', (reason, p) => {
315320
logger.error(`Unhandled Rejection at: Promise: ${JSON.stringify(p)}, reason: ${reason}`);
316321
});

0 commit comments

Comments
 (0)