diff --git a/.github/workflows/acceptance.yml b/.github/workflows/acceptance.yml index 4b753d3f9b..14b4dd6ad7 100644 --- a/.github/workflows/acceptance.yml +++ b/.github/workflows/acceptance.yml @@ -100,6 +100,12 @@ jobs: with: testfilter: cache-service + server-config: + name: Server Config + uses: ./.github/workflows/acceptance-workflow.yml + with: + testfilter: serverconfig + publish_results: name: Publish Results if: ${{ !cancelled() }} diff --git a/docs/configuration.md b/docs/configuration.md index 1e62c41cc9..3b2c7861d7 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -39,6 +39,7 @@ Unless you need to set a non-default value, it is recommended to only populate o | `RATE_LIMIT_DISABLED` | "false" | Flag to disable IP based rate limiting. | | `REQUEST_ID_IS_OPTIONAL` | "" | Flag to set it the JSON RPC request id field in the body should be optional. Note, this breaks the API spec and is not advised and is provided for test purposes only where some wallets may be non compliant | | `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/hashgraph/hedera-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) | ## Relay diff --git a/package-lock.json b/package-lock.json index fbc4a15394..916887f7cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9308,6 +9308,7 @@ "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.5.tgz", "integrity": "sha512-HTm14iMQKK2FjFLRTM5lAVcyaUzOnqbPtesFIvREgXpJHdQm8bWS+GkQgIkfaBYRHuCnea7w8UVNfwiAQhlr9A==", "dev": true, + "hasInstallScript": true, "optional": true, "dependencies": { "node-gyp-build": "^4.3.0" @@ -9670,6 +9671,7 @@ "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.7.tgz", "integrity": "sha512-vLt1O5Pp+flcArHGIyKEQq883nBt8nN8tVBcoL0qUXj2XT1n7p70yGIq2VK98I5FdZ1YHc0wk/koOnHjnXWk1Q==", "dev": true, + "hasInstallScript": true, "optional": true, "dependencies": { "node-gyp-build": "^4.3.0" diff --git a/package.json b/package.json index f7459e722c..4e76bacbfb 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "acceptancetest:precompile-calls": "ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@precompile-calls' --exit", "acceptancetest:cache-service": "ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@cache-service' --exit", "acceptancetest:rpc_api_schema_conformity": "ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@api-conformity' --exit", + "acceptancetest:serverconfig": "ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@server-config' --exit", "build": "npx lerna run build", "build-and-test": "npx lerna run build && npx lerna run test", "build:docker": "docker build . -t ${npm_package_name}", diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index c985a6e6a6..4b0435542d 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -19,9 +19,13 @@ */ import app from './server'; +import { setServerTimeout } from './koaJsonRpc/lib/utils'; // Import the 'setServerTimeout' function from the correct location async function main() { - await app.listen({ port: process.env.SERVER_PORT || 7546 }); + const server = await app.listen({ port: process.env.SERVER_PORT || 7546 }); + + // set request timeout to ensure sockets are closed after specified time of inactivity + setServerTimeout(server); } main(); diff --git a/packages/server/src/koaJsonRpc/lib/utils.ts b/packages/server/src/koaJsonRpc/lib/utils.ts new file mode 100644 index 0000000000..e912dee1fd --- /dev/null +++ b/packages/server/src/koaJsonRpc/lib/utils.ts @@ -0,0 +1,26 @@ +/*- + * + * Hedera JSON RPC Relay + * + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import type { Server } from 'http'; + +export function setServerTimeout(server: Server): void { + const requestTimeoutMs = parseInt(process.env.SERVER_REQUEST_TIMEOUT_MS ?? '60000'); + server.setTimeout(requestTimeoutMs); +} diff --git a/packages/server/tests/acceptance/index.spec.ts b/packages/server/tests/acceptance/index.spec.ts index 196135aad9..1c2104338a 100644 --- a/packages/server/tests/acceptance/index.spec.ts +++ b/packages/server/tests/acceptance/index.spec.ts @@ -45,6 +45,7 @@ import constants from '@hashgraph/json-rpc-relay/dist/lib/constants'; // Utils and types import { Utils } from '../helpers/utils'; import { AliasAccount } from '../types/AliasAccount'; +import { setServerTimeout } from '../../src/koaJsonRpc/lib/utils'; chai.use(chaiAsPromised); dotenv.config({ path: path.resolve(__dirname, '../../../.env') }); @@ -185,6 +186,7 @@ describe('RPC Server Acceptance Tests', function () { logger.info(`Start relay on port ${constants.RELAY_PORT}`); relayServer = app.listen({ port: constants.RELAY_PORT }); + setServerTimeout(relayServer); if (process.env.TEST_WS_SERVER === 'true') { logger.info(`Start ws-server on port ${constants.WEB_SOCKET_PORT}`); diff --git a/packages/server/tests/acceptance/rateLimiter.spec.ts b/packages/server/tests/acceptance/rateLimiter.spec.ts index 8e206a7144..0245439a32 100644 --- a/packages/server/tests/acceptance/rateLimiter.spec.ts +++ b/packages/server/tests/acceptance/rateLimiter.spec.ts @@ -2,7 +2,7 @@ * * Hedera JSON RPC Relay * - * Copyright (C) 2023 Hedera Hashgraph, LLC + * Copyright (C) 2023-2024 Hedera Hashgraph, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/server/tests/acceptance/serverConfig.spec.ts b/packages/server/tests/acceptance/serverConfig.spec.ts new file mode 100644 index 0000000000..2a10322262 --- /dev/null +++ b/packages/server/tests/acceptance/serverConfig.spec.ts @@ -0,0 +1,41 @@ +/*- + * + * Hedera JSON RPC Relay + * + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { expect } from 'chai'; +import { Utils } from '../helpers/utils'; + +describe('@server-config Server Configuration Options Coverage', function () { + describe('Koa Server Timeout', () => { + it('should timeout a request after the specified time', async () => { + const requestTimeoutMs: number = parseInt(process.env.SERVER_REQUEST_TIMEOUT_MS || '3000'); + const host = 'localhost'; + const port = parseInt(process.env.SERVER_PORT || '7546'); + const method = 'eth_blockNumber'; + const params: any[] = []; + + try { + await Utils.sendJsonRpcRequestWithDelay(host, port, method, params, requestTimeoutMs + 1000); + throw new Error('Request did not timeout as expected'); // Force the test to fail if the request does not time out + } catch (err) { + expect(err.code).to.equal('ECONNRESET'); + expect(err.message).to.equal('socket hang up'); + } + }); + }); +}); diff --git a/packages/server/tests/helpers/utils.ts b/packages/server/tests/helpers/utils.ts index 9cc3d229c7..0c3fc3ceae 100644 --- a/packages/server/tests/helpers/utils.ts +++ b/packages/server/tests/helpers/utils.ts @@ -27,6 +27,7 @@ import RelayCall from '../../tests/helpers/constants'; import { AccountId, KeyList, PrivateKey } from '@hashgraph/sdk'; import { AliasAccount } from '../types/AliasAccount'; import ServicesClient from '../clients/servicesClient'; +import http from 'http'; export class Utils { /** @@ -308,4 +309,63 @@ export class Utils { } return accounts; } + + static sendJsonRpcRequestWithDelay( + host: string, + port: number, + method: string, + params: any[], + delayMs: number, + ): Promise { + const requestData = JSON.stringify({ + jsonrpc: '2.0', + method: method, + params: params, + id: 1, + }); + + const options = { + hostname: host, + port: port, + path: '/', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(requestData), + }, + timeout: delayMs, + }; + + return new Promise((resolve, reject) => { + // setup the request + const req = http.request(options, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + resolve(JSON.parse(data)); + }); + }); + + // handle request errors for testing purposes + req.on('timeout', () => { + req.destroy(); + reject(new Error(`Request timed out after ${delayMs}ms`)); + }); + + req.on('error', (err) => { + reject(err); + }); + + // Introduce a delay with inactivity, before sending the request + setTimeout(async () => { + req.write(requestData); + req.end(); + await new Promise((r) => setTimeout(r, delayMs + 1000)); + }, delayMs); + }); + } } diff --git a/packages/server/tests/localAcceptance.env b/packages/server/tests/localAcceptance.env index 546464ccea..f30b06bf7a 100644 --- a/packages/server/tests/localAcceptance.env +++ b/packages/server/tests/localAcceptance.env @@ -23,3 +23,4 @@ TEST_GAS_PRICE_DEVIATION=0.2 WS_NEW_HEADS_ENABLED=false INITIAL_BALANCE='5000000000' LIMIT_DURATION=90000 +SERVER_REQUEST_TIMEOUT_MS=60000 diff --git a/packages/server/tests/previewnetAcceptance.env b/packages/server/tests/previewnetAcceptance.env index f58f3db13d..81130cc0ac 100644 --- a/packages/server/tests/previewnetAcceptance.env +++ b/packages/server/tests/previewnetAcceptance.env @@ -16,4 +16,5 @@ FILTER_API_ENABLED=true DEBUG_API_ENABLED=true TEST_GAS_PRICE_DEVIATION=0.2 WS_NEW_HEADS_ENABLED=true +SERVER_REQUEST_TIMEOUT_MS=60000 diff --git a/packages/server/tests/testnetAcceptance.env b/packages/server/tests/testnetAcceptance.env index 3ee181d349..b074e2e293 100644 --- a/packages/server/tests/testnetAcceptance.env +++ b/packages/server/tests/testnetAcceptance.env @@ -15,3 +15,4 @@ SUBSCRIPTIONS_ENABLED=true FILTER_API_ENABLED=true DEBUG_API_ENABLED=true TEST_GAS_PRICE_DEVIATION=0.2 +SERVER_REQUEST_TIMEOUT_MS=60000