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
1 change: 1 addition & 0 deletions .env.http.example
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ OPERATOR_KEY_MAIN= # Operator private key used to sign transaction
# TXPOOL_API_ENABLED=true # Enables txpool related methods
# FILTER_API_ENABLED=true # Enables filter related methods
# ESTIMATE_GAS_THROWS=true # If true, throws actual error reason during contract reverts
# VALID_JSON_RPC_HTTP_REQUESTS_STATUS_CODE=true # If true, returns HTTP 200 for JSON-RPC errors

# ========== BATCH REQUESTS ==========
# BATCH_REQUESTS_ENABLED=true # Enable or disable batch requests
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/acceptance-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ on:
required: false
default: false
type: boolean
valid_json_rpc_http_requests_status_code:
required: false
default: false
type: boolean
envfile:
required: false
default: localAcceptance.env
Expand Down Expand Up @@ -104,6 +108,7 @@ jobs:
GITHUB_PR_NUMBER: ${{ github.event.number }}
GITHUB_REPOSITORY: ${{ github.repository }}
OPERATOR_KEY: ${{ secrets.operator_key }}
VALID_JSON_RPC_HTTP_REQUESTS_STATUS_CODE: "${{ inputs.valid_json_rpc_http_requests_status_code }}"

- name: Upload Heap Snapshots
if: ${{ !cancelled() }}
Expand All @@ -117,7 +122,7 @@ jobs:
if: always()
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: Test Results (${{ inputs.testfilter }})
name: Test Results (${{ inputs.testfilter }})${{ inputs.valid_json_rpc_http_requests_status_code && ' 200' || '' }}
path: test-*.xml

- name: Upload coverage report
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/acceptance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,15 @@ jobs:
- { name: 'Websocket Batch 3', testfilter: 'ws_batch3', test_ws_server: true }
- { name: 'Cache Service', testfilter: 'cache-service' }
- { name: 'Server Config', testfilter: 'serverconfig' }
- { name: 'Semantics and varying response status', testfilter: 'json_rpc_compliance' }
- name: "Semantics and default OK response status"
testfilter: "json_rpc_compliance"
valid_json_rpc_http_requests_status_code: true
uses: ./.github/workflows/acceptance-workflow.yml
with:
testfilter: ${{ matrix.test.testfilter }}
test_ws_server: ${{ matrix.test.test_ws_server || false }}
valid_json_rpc_http_requests_status_code: ${{ matrix.test.valid_json_rpc_http_requests_status_code || false }}
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

Expand Down
1 change: 1 addition & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ Unless you need to set a non-default value, it is recommended to only populate o
| `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. |
| `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) |
| `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) |
| `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. |

## WS-Server

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"acceptancetest:rpc_api_schema_conformity": "c8 ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@api-conformity' --exit",
"acceptancetest:serverconfig": "c8 ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@server-config' --exit",
"acceptancetest:send_raw_transaction_extension": "c8 ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@sendRawTransactionExtension' --exit",
"acceptancetest:json_rpc_compliance": "c8 ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@json-rpc-compliance' --exit",
"acceptancetest:debug": "c8 ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@debug' --exit",
"acceptancetest:xts": "c8 ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@xts' --exit",
"build": "npx lerna run build",
Expand Down
5 changes: 5 additions & 0 deletions packages/config-service/src/services/globalConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,11 @@ const _CONFIG = {
required: false,
defaultValue: 10,
},
VALID_JSON_RPC_HTTP_REQUESTS_STATUS_CODE: {
type: 'boolean',
required: false,
defaultValue: false,
},
} as const satisfies { [key: string]: ConfigProperty }; // Ensures _CONFIG is read-only and conforms to the ConfigProperty structure

export type ConfigKey = keyof typeof _CONFIG;
Expand Down
67 changes: 67 additions & 0 deletions packages/server/src/compliance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: Apache-2.0

import { ConfigService } from '@hashgraph/json-rpc-config-service/dist/services';
import { ParameterizedContext } from 'koa';

interface IResponseContext {
body: {
jsonrpc: unknown;
id: unknown;
result?: unknown;
error?: { code: unknown; message: unknown };
};
status: number | undefined;
}

const VALID_JSON_RPC_HTTP_REQUESTS_STATUS_CODE = ConfigService.get('VALID_JSON_RPC_HTTP_REQUESTS_STATUS_CODE');

const FALLBACK_RESPONSE_BODY = {
jsonrpc: '2.0',
id: null,
error: { code: -32600, message: 'Request body is empty; expected a JSON-RPC 2.0 request' },
};

export const INVALID_METHOD_RESPONSE_BODY = {
...FALLBACK_RESPONSE_BODY,
error: { code: -32600, message: 'Invalid HTTP method: only POST is allowed' },
};

const makeSureBodyExistsAndCanBeChecked = (ctx: IResponseContext) => {
if (ctx.status === 200) return false;

if (!ctx.body) {
ctx.status = 400;
ctx.body = structuredClone(FALLBACK_RESPONSE_BODY);
return false;
}

if (Array.isArray(ctx.body)) {
ctx.status = 200;
return false;
}

if (typeof ctx.body !== 'object') {
ctx.status = 400;
ctx.body = structuredClone(FALLBACK_RESPONSE_BODY);
return false;
}
if (!ctx.body.jsonrpc) ctx.body.jsonrpc = FALLBACK_RESPONSE_BODY.jsonrpc;
if (!ctx.body.id) ctx.body.id = FALLBACK_RESPONSE_BODY.id;

return true;
};

/**
* Ensures a JSON-RPC response uses a valid JSON-RPC 2.0 structure.
* Normalizes missing or invalid fields for both single and batch responses.
* May update HTTP status depending on VALID_JSON_RPC_HTTP_REQUESTS_STATUS_CODE.
*
* @param {IResponseContext & ParameterizedContext} ctx - Koa context containing status and body.
*/
export const jsonRpcComplianceLayer = (ctx: IResponseContext & ParameterizedContext) => {
if (!makeSureBodyExistsAndCanBeChecked(ctx)) return;
if (ctx.status === 400) {
if (!ctx.body.error?.code) ctx.body.error = structuredClone(FALLBACK_RESPONSE_BODY.error);
if (VALID_JSON_RPC_HTTP_REQUESTS_STATUS_CODE) ctx.status = 200;
}
};
12 changes: 7 additions & 5 deletions packages/server/src/koaJsonRpc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,13 @@ export default class KoaJsonRpc {

// verify max batch size
if (body.length > this.batchRequestsMaxSize) {
ctx.body = jsonRespError(
null,
predefined.BATCH_REQUESTS_AMOUNT_MAX_EXCEEDED(body.length, this.batchRequestsMaxSize),
requestId,
);
ctx.body = [
jsonRespError(
null,
predefined.BATCH_REQUESTS_AMOUNT_MAX_EXCEEDED(body.length, this.batchRequestsMaxSize),
requestId,
),
];
ctx.status = 400;
ctx.state.status = `${ctx.status} (${INVALID_REQUEST})`;
return;
Expand Down
5 changes: 4 additions & 1 deletion packages/server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import pino from 'pino';
import { collectDefaultMetrics, Histogram, Registry } from 'prom-client';
import { v4 as uuid } from 'uuid';

import { INVALID_METHOD_RESPONSE_BODY, jsonRpcComplianceLayer } from './compliance';
import { formatRequestIdMessage } from './formatters';
import KoaJsonRpc from './koaJsonRpc';
import { spec } from './koaJsonRpc/lib/RpcError';
Expand Down Expand Up @@ -277,7 +278,8 @@ export async function initializeServer() {
// support CORS preflight
ctx.status = 200;
} else {
logger.warn(`skipping HTTP method: [${ctx.method}], url: ${ctx.url}, status: ${ctx.status}`);
ctx.status = ConfigService.get('VALID_JSON_RPC_HTTP_REQUESTS_STATUS_CODE') ? 200 : 400;
ctx.body = structuredClone(INVALID_METHOD_RESPONSE_BODY);
}
});

Expand Down Expand Up @@ -309,6 +311,7 @@ export async function initializeServer() {

app.use(async (ctx) => {
await rpcApp(ctx);
jsonRpcComplianceLayer(ctx);
});

process.on('unhandledRejection', (reason, p) => {
Expand Down
Loading
Loading