From 3436b215ce1be58ab39fbb8d3df939ec28e1d922 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Tue, 5 Mar 2024 15:16:09 -0500 Subject: [PATCH] enhance info endpoint and bump to `v0.1.13` (#117) In a previous PR some(much desired) version information was added to the `/info` endpoint, however in certain environments(our docker container) this blows up. This PR prevents the endpoint from blowing up, while also providing some optionality and defaults to fall back on and bumps version to `v0.1.13` Added the text below to the `README.md` file: - `server` is read from the `process.env.npm_package_name` variable that `npm` provides. If that does not exist, it will check for a `DWN_SERVER_PACKAGE_NAME` environment variable set by the user, or otherwise it will default to `@web5/dwn-server`. - `version` and `sdkVersion` are read from the `package.json` file. It will locate the file's path either from the `process.env.npm_package_json` variable that `npm` provides. If that does not exist, it will check for a `DWN_SERVER_PACKAGE_JSON` environment variable set by the user, or otherwise it will default to `/dwn-server/package.json` which is the path within the default Docker container build. --- README.md | 6 +- package-lock.json | 4 +- package.json | 2 +- src/config.ts | 16 +++++ src/http-api.ts | 21 +++++-- src/json-rpc-handlers/dwn/process-message.ts | 2 +- tests/http-api.spec.ts | 64 ++++++++++++++++++++ 7 files changed, 106 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index b5b50e1..cf5eca0 100644 --- a/README.md +++ b/README.md @@ -321,6 +321,10 @@ the server exposes information about itself via the `/info` endpoint, which retu "maxFileSize": 1073741824, "registrationRequirements": ["proof-of-work-sha256-v0", "terms-of-service"], "version": "0.1.5", - "sdkVersion": "0.2.6" + "sdkVersion": "0.2.6", + "webSocketSupport": "true" } ``` + +- `server` is read from the `process.env.npm_package_name` variable that `npm` provides. If that does not exist, it will check for a `DWN_SERVER_PACKAGE_NAME` environment variable set by the user, or otherwise it will default to `@web5/dwn-server`. +- `version` and `sdkVersion` are read from the `package.json` file. It will locate the file's path either from the `process.env.npm_package_json` variable that `npm` provides. If that does not exist, it will check for a `DWN_SERVER_PACKAGE_JSON` environment variable set by the user, or otherwise it will default to `/dwn-server/package.json` which is the path within the default Docker container build. diff --git a/package-lock.json b/package-lock.json index bd5ee4a..3ec8af0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@web5/dwn-server", - "version": "0.1.12", + "version": "0.1.13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@web5/dwn-server", - "version": "0.1.12", + "version": "0.1.13", "dependencies": { "@tbd54566975/dwn-sdk-js": "0.2.18", "@tbd54566975/dwn-sql-store": "0.2.10", diff --git a/package.json b/package.json index 20bad08..7025a03 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@web5/dwn-server", "type": "module", - "version": "0.1.12", + "version": "0.1.13", "files": [ "dist", "src" diff --git a/src/config.ts b/src/config.ts index 4d9c3f1..ee1f895 100644 --- a/src/config.ts +++ b/src/config.ts @@ -3,6 +3,22 @@ import bytes from 'bytes'; export type DwnServerConfig = typeof config; export const config = { + /** + * Used to populate the `server` property returned by the `/info` endpoint. + * + * If running using `npm` the `process.env.npm_package_name` variable exists and we use that, + * otherwise we fall back on the use defined `DWN_SERVER_PACKAGE_NAME` or `@web5/dwn-server`. + */ + serverName: process.env.npm_package_name || process.env.DWN_SERVER_PACKAGE_NAME || '@web5/dwn-server', + /** + * Used to populate the `version` and `sdkVersion` properties returned by the `/info` endpoint. + * + * The `version` and `sdkVersion` are pulled from `package.json` at runtime. + * If running using `npm` the `process.env.npm_package_json` variable exists as the filepath, so we use that. + * Otherwise we check to see if a specific `DWN_SERVER_PACKAGE_JSON` exists, if it does we use that. + * Finally if both of those options don't exist we resort to the path within the docker server image, located at `/dwn-server/package.json` + */ + packageJsonPath: process.env.npm_package_json || process.env.DWN_SERVER_PACKAGE_JSON || '/dwn-server/package.json', // max size of data that can be provided with a RecordsWrite maxRecordDataSize: bytes(process.env.MAX_RECORD_DATA_SIZE || '1gb'), // port that server listens on diff --git a/src/http-api.ts b/src/http-api.ts index 19b4680..8abfa61 100644 --- a/src/http-api.ts +++ b/src/http-api.ts @@ -21,10 +21,10 @@ import { jsonRpcRouter } from './json-rpc-api.js'; import { requestCounter, responseHistogram } from './metrics.js'; import type { RegistrationManager } from './registration/registration-manager.js'; -const packageJson = process.env.npm_package_json ? JSON.parse(readFileSync(process.env.npm_package_json).toString()) : {}; export class HttpApi { #config: DwnServerConfig; + #packageInfo: { version?: string, sdkVersion?: string, server: string }; #api: Express; #server: http.Server; registrationManager: RegistrationManager; @@ -33,6 +33,19 @@ export class HttpApi { constructor(config: DwnServerConfig, dwn: Dwn, registrationManager?: RegistrationManager) { console.log(config); + this.#packageInfo = { + server: config.serverName, + }; + + try { + // We populate the `version` and `sdkVersion` properties from the `package.json` file. + const packageJson = JSON.parse(readFileSync(config.packageJsonPath).toString()); + this.#packageInfo.version = packageJson.version; + this.#packageInfo.sdkVersion = packageJson.dependencies ? packageJson.dependencies['@tbd54566975/dwn-sdk-js'] : undefined; + } catch (error: any) { + log.error('could not read `package.json` for version info', error); + } + this.#config = config; this.#api = express(); this.#server = http.createServer(this.#api); @@ -185,11 +198,11 @@ export class HttpApi { } res.json({ - server : process.env.npm_package_name, + server : this.#packageInfo.server, maxFileSize : config.maxRecordDataSize, registrationRequirements : registrationRequirements, - version : packageJson.version, - sdkVersion : packageJson.dependencies['@tbd54566975/dwn-sdk-js'], + version : this.#packageInfo.version, + sdkVersion : this.#packageInfo.sdkVersion, webSocketSupport : config.webSocketSupport, }); }); diff --git a/src/json-rpc-handlers/dwn/process-message.ts b/src/json-rpc-handlers/dwn/process-message.ts index bed6db3..cac2090 100644 --- a/src/json-rpc-handlers/dwn/process-message.ts +++ b/src/json-rpc-handlers/dwn/process-message.ts @@ -115,7 +115,7 @@ export const handleDwnProcessMessage: JsonRpcHandler = async ( ); // log the unhandled error response - log.error('handleDwnProcessMessage error', jsonRpcResponse, e); + log.error('handleDwnProcessMessage error', jsonRpcResponse, dwnRequest, e); return { jsonRpcResponse } as HandlerResponse; } }; diff --git a/tests/http-api.spec.ts b/tests/http-api.spec.ts index 621660d..eabd370 100644 --- a/tests/http-api.spec.ts +++ b/tests/http-api.spec.ts @@ -1,4 +1,5 @@ // node.js 18 and earlier, needs globalThis.crypto polyfill +import sinon from 'sinon'; import { Cid, DataStream, @@ -18,6 +19,7 @@ import request from 'supertest'; import { v4 as uuidv4 } from 'uuid'; import { config } from '../src/config.js'; +import log from 'loglevel'; import { HttpApi } from '../src/http-api.js'; import type { JsonRpcErrorResponse, @@ -71,6 +73,7 @@ describe('http api', function () { }); beforeEach(async function () { + sinon.restore(); server = await httpApi.start(3000); }); @@ -80,6 +83,7 @@ describe('http api', function () { }); after(function () { + sinon.restore(); clock.restore(); }); @@ -561,6 +565,66 @@ describe('http api', function () { info = await resp.json(); expect(info['server']).to.equal('@web5/dwn-server'); expect(info['webSocketSupport']).to.equal(false); + + // restore old config value + config.webSocketSupport = true; + }); + + it('verify /info still returns when package.json file does not exist', async function () { + server.close(); + server.closeAllConnections(); + + // set up spy to check for an error log by the server + const logSpy = sinon.spy(log, 'error'); + + // set the config to an invalid file path + const packageJsonConfig = config.packageJsonPath; + config.packageJsonPath = '/some/invalid/file.json'; + httpApi = new HttpApi(config, dwn, registrationManager); + server = await httpApi.start(3000); + + const resp = await fetch(`http://localhost:3000/info`); + const info = await resp.json(); + expect(resp.status).to.equal(200); + + // check that server name exists in the info object + expect(info['server']).to.equal('@web5/dwn-server'); + + // check that `sdkVersion` and `version` are undefined as they were not abel to be retrieved from the invalid file. + expect(info['sdkVersion']).to.be.undefined; + expect(info['version']).to.be.undefined; + + // check the logSpy was called + expect(logSpy.callCount).to.equal(1); + expect(logSpy.args[0][0]).to.contain('could not read `package.json` for version info'); + + // restore old config path + config.packageJsonPath = packageJsonConfig; + }); + + it('verify /info returns server name from config', async function () { + server.close(); + server.closeAllConnections(); + + // set a custom name for the `serverName` + const serverName = config.serverName; + config.serverName = '@web5/dwn-server-2' + httpApi = new HttpApi(config, dwn, registrationManager); + server = await httpApi.start(3000); + + const resp = await fetch(`http://localhost:3000/info`); + const info = await resp.json(); + expect(resp.status).to.equal(200); + + // verify that the custom server name was passed to the info endpoint + expect(info['server']).to.equal('@web5/dwn-server-2'); + + // verify that `sdkVersion` and `version` exist. + expect(info['sdkVersion']).to.not.be.undefined; + expect(info['version']).to.not.be.undefined; + + // restore server name config + config.serverName = serverName; }); }); });