Skip to content

Commit c096109

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

File tree

11 files changed

+633
-18
lines changed

11 files changed

+633
-18
lines changed

.env.http.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ OPERATOR_KEY_MAIN= # Operator private key used to sign transaction
2323
# TXPOOL_API_ENABLED=true # Enables txpool related methods
2424
# FILTER_API_ENABLED=true # Enables filter related methods
2525
# ESTIMATE_GAS_THROWS=true # If true, throws actual error reason during contract reverts
26+
# VALID_JSON_RPC_HTTP_REQUESTS_STATUS_CODE=true # If true, returns HTTP 200 for JSON-RPC errors
2627

2728
# ========== BATCH REQUESTS ==========
2829
# BATCH_REQUESTS_ENABLED=true # Enable or disable batch requests

.github/workflows/acceptance-workflow.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ on:
1010
required: false
1111
default: false
1212
type: boolean
13+
valid_json_rpc_http_requests_status_code:
14+
required: false
15+
default: false
16+
type: boolean
1317
envfile:
1418
required: false
1519
default: localAcceptance.env
@@ -104,6 +108,7 @@ jobs:
104108
GITHUB_PR_NUMBER: ${{ github.event.number }}
105109
GITHUB_REPOSITORY: ${{ github.repository }}
106110
OPERATOR_KEY: ${{ secrets.operator_key }}
111+
VALID_JSON_RPC_HTTP_REQUESTS_STATUS_CODE: ${{ inputs.valid_json_rpc_http_requests_status_code }}
107112

108113
- name: Upload Heap Snapshots
109114
if: ${{ !cancelled() }}

.github/workflows/acceptance.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ jobs:
3838
- { name: 'Websocket Batch 3', testfilter: 'ws_batch3', test_ws_server: true }
3939
- { name: 'Cache Service', testfilter: 'cache-service' }
4040
- { name: 'Server Config', testfilter: 'serverconfig' }
41+
- { name: 'Semantics and varying response status', testfilter: 'json_rpc_compliance' }
42+
- name: "Semantics and default OK response status"
43+
testfilter: "json_rpc_compliance"
44+
valid_json_rpc_http_requests_status_code: true
4145
uses: ./.github/workflows/acceptance-workflow.yml
4246
with:
4347
testfilter: ${{ matrix.test.testfilter }}

docs/configuration.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ Unless you need to set a non-default value, it is recommended to only populate o
133133
| `SERVER_HOST` | undefined | The hostname or IP address on which the server listens for incoming connections. If `SERVER_HOST` is not configured or left undefined (same as `0.0.0.0`), it permits external connections by default, offering more flexibility. |
134134
| `SERVER_PORT` | "7546" | The RPC server port number to listen for requests on. Currently a static value defaulting to 7546. See [#955](https://github.com/hiero-ledger/hiero-json-rpc-relay/issues/955) |
135135
| `SERVER_REQUEST_TIMEOUT_MS` | "60000" | The time of inactivity allowed before a timeout is triggered and the socket is closed. See [NodeJs Server Timeout](https://nodejs.org/api/http.html#serversettimeoutmsecs-callback) |
136+
| `VALID_JSON_RPC_HTTP_REQUESTS_STATUS_CODE` | "false" | Setting this flag to "true" will cause the JSON-RPC relay server to always return an HTTP 200 status code for errors that comply with the JSON-RPC API standard, when a correctly formed request following the standard is received. |
136137

137138
## WS-Server
138139

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
"acceptancetest:rpc_api_schema_conformity": "c8 ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@api-conformity' --exit",
6868
"acceptancetest:serverconfig": "c8 ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@server-config' --exit",
6969
"acceptancetest:send_raw_transaction_extension": "c8 ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@sendRawTransactionExtension' --exit",
70+
"acceptancetest:json_rpc_compliance": "c8 ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@json-rpc-compliance' --exit",
7071
"acceptancetest:debug": "c8 ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@debug' --exit",
7172
"acceptancetest:xts": "c8 ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@xts' --exit",
7273
"build": "npx lerna run build",

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: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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.status === 200) return false;
31+
32+
if (!ctx.body) {
33+
ctx.status = 400;
34+
ctx.body = structuredClone(FALLBACK_RESPONSE_BODY);
35+
return false;
36+
}
37+
38+
if (Array.isArray(ctx.body)) {
39+
ctx.status = 200;
40+
return false;
41+
}
42+
43+
if (typeof ctx.body !== 'object') {
44+
ctx.status = 400;
45+
ctx.body = structuredClone(FALLBACK_RESPONSE_BODY);
46+
return false;
47+
}
48+
if (!ctx.body.jsonrpc) ctx.body.jsonrpc = FALLBACK_RESPONSE_BODY.jsonrpc;
49+
if (!ctx.body.id) ctx.body.id = FALLBACK_RESPONSE_BODY.id;
50+
51+
return true;
52+
};
53+
54+
/**
55+
* Ensures a JSON-RPC response uses a valid JSON-RPC 2.0 structure.
56+
* Normalizes missing or invalid fields for both single and batch responses.
57+
* May update HTTP status depending on VALID_JSON_RPC_HTTP_REQUESTS_STATUS_CODE.
58+
*
59+
* @param {IResponseContext & ParameterizedContext} ctx - Koa context containing status and body.
60+
*/
61+
export const jsonRpcComplianceLayer = (ctx: IResponseContext & ParameterizedContext) => {
62+
if (!makeSureBodyExistsAndCanBeChecked(ctx)) return;
63+
if (ctx.status === 400) {
64+
if (!ctx.body.error?.code) ctx.body.error = structuredClone(FALLBACK_RESPONSE_BODY.error);
65+
if (VALID_JSON_RPC_HTTP_REQUESTS_STATUS_CODE) ctx.status = 200;
66+
}
67+
};

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: 4 additions & 1 deletion
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 = ConfigService.get('VALID_JSON_RPC_HTTP_REQUESTS_STATUS_CODE') ? 200 : 400;
282+
ctx.body = structuredClone(INVALID_METHOD_RESPONSE_BODY);
281283
}
282284
});
283285

@@ -309,6 +311,7 @@ export async function initializeServer() {
309311

310312
app.use(async (ctx) => {
311313
await rpcApp(ctx);
314+
jsonRpcComplianceLayer(ctx);
312315
});
313316

314317
process.on('unhandledRejection', (reason, p) => {

0 commit comments

Comments
 (0)