From c4e280077bd06f35bec0ffb8eb965c502e573d11 Mon Sep 17 00:00:00 2001 From: peter Date: Mon, 20 Jan 2020 00:01:25 +0100 Subject: [PATCH] Better ErrorHandler and more tests --- package.json | 2 +- src/Backend.ts | 11 ++--- src/Frontend.ts | 9 +--- src/Types.ts | 3 +- src/Utils.ts | 94 +++++++++++++++++++++++------------ test/Test.ts | 127 +++++++++++++++++++++++++++++++++--------------- 6 files changed, 162 insertions(+), 84 deletions(-) diff --git a/package.json b/package.json index cba6760..600b40c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rpclibrary", - "version": "1.5.3", + "version": "1.6.0", "description": "rpclibrary is a websocket on steroids!", "main": "./js/Index.js", "repository": { diff --git a/src/Backend.ts b/src/Backend.ts index be355c6..e3ad92a 100644 --- a/src/Backend.ts +++ b/src/Backend.ts @@ -42,18 +42,15 @@ export class RPCServer< this.errorHandler = (socket:I.Socket) => (error:any) => { if(conf.errorHandler) conf.errorHandler(socket, error) - socket.destroy(); - console.error("Caught websocket error", String(error)) + else throw error } this.closeHandler = (socket:I.Socket) => { - if(!conf.closeHandler) console.log("Connection on port "+socket.port+" closing") - else conf.closeHandler(socket) + if(conf.closeHandler) conf.closeHandler(socket) } this.connectionHandler = (socket:I.Socket) => { - if(!conf.connectionHandler) console.log("New connection on port "+socket.port) - else conf.connectionHandler(socket) + if(conf.connectionHandler) conf.connectionHandler(socket) } let badRPC @@ -86,7 +83,7 @@ export class RPCServer< protected initRPCs(socket:I.Socket){ socket.hook('info', () => rpcInfos) const rpcInfos:T.ExtendedRpcInfo[] = [ - ...this.exporters.flatMap(exporter => U.rpcHooker(socket, exporter, this.sesame)) + ...this.exporters.flatMap(exporter => U.rpcHooker(socket, exporter, this.errorHandler, this.sesame)) ] } diff --git a/src/Frontend.ts b/src/Frontend.ts index 99cfaab..1796f0b 100644 --- a/src/Frontend.ts +++ b/src/Frontend.ts @@ -4,14 +4,9 @@ import bsock = require('bsock'); import * as T from './Types'; import * as I from './Interfaces'; +import { stripAfterEquals } from './Utils'; + -/** - * Utility function to strip parameters like "a = 3" of their defaults - * @param str The parameter to modify - */ -function stripAfterEquals(str:string):string{ - return str.split("=")[0] -} /** * A websocket-on-steroids with built-in RPC capabilities diff --git a/src/Types.ts b/src/Types.ts index 2b571be..dc66722 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -8,7 +8,7 @@ export type ConnectionHandler = (socket:I.Socket) => void export type ErrorHandler = (socket:I.Socket, error:any) => void export type CloseHandler = (socket:I.Socket) => void export type SesameFunction = (sesame : string) => boolean - +export type ExceptionHandling = 'local' | 'remote' export type SesameConf = { sesame?: string | SesameFunction } @@ -17,6 +17,7 @@ export type ServerConf = { errorHandler?: ErrorHandler closeHandler?: CloseHandler visibility?: Visibility + exceptionHandling?: ExceptionHandling } & SesameConf export type SocketConf = { diff --git a/src/Utils.ts b/src/Utils.ts index 24ec878..97e8352 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -10,8 +10,7 @@ import { SubscriptionResponse } from "./Types"; * @param owner The owning RPC group's name * @throws Error on RPC without name property */ -export const rpcToRpcinfo = (rpc : T.RPC, owner: string, sesame?:T.SesameFunction):T.RpcInfo => { - +export const rpcToRpcinfo = (rpc : T.RPC, owner: string, errorHandler: T.ErrorHandler, sesame?:T.SesameFunction):T.RpcInfo => { switch (typeof rpc){ case "object": if(rpc['call']){ @@ -20,10 +19,10 @@ export const rpcToRpcinfo = (rpc : T.RPC, owner argNames: extractArgs(rpc['call']), type: "Call", name: rpc.name, - call: sesame?async (_sesame, ...args) => {if(sesame(_sesame)) return await rpc['call'].apply({}, args); throw new Error('Bad sesame')}:rpc['call'], // check & remove sesame + call: sesame?async (_sesame, ...args) => {if(sesame(_sesame)) return await rpc['call'].apply({}, args); throw makeError(rpc.name)}:rpc['call'], // check & remove sesame } }else{ - const generator = hookGenerator(>rpc, sesame) + const generator = hookGenerator(>rpc, errorHandler, sesame) return { owner: owner, argNames: extractArgs(generator(undefined)), @@ -45,7 +44,7 @@ RPC did not provide a name. argNames: extractArgs(rpc), type: "Call", name: rpc.name, - call: sesame?async (_sesame, ...args) => {if(sesame(_sesame)) return await rpc.apply({}, args)}:rpc, // check & remove sesame + call: sesame?async (_sesame, ...args) => {if(sesame(_sesame)) return await rpc.apply({}, args); throw makeError(rpc.name)}:rpc, // check & remove sesame } } throw new Error("Bad socketIORPC type "+ typeof rpc) @@ -57,62 +56,97 @@ RPC did not provide a name. * @param exporter The exporter * @param makeUnique @default true Attach a suffix to RPC names */ -export function rpcHooker(socket: I.Socket, exporter:I.RPCExporter, sesame?:T.SesameFunction, makeUnique = true):T.ExtendedRpcInfo[]{ +export function rpcHooker(socket: I.Socket, exporter:I.RPCExporter, errorHandler: T.ErrorHandler, sesame?:T.SesameFunction, makeUnique = true):T.ExtendedRpcInfo[]{ const owner = exporter.name const RPCs = [...exporter.exportRPCs()] - return RPCs.map(rpc => rpcToRpcinfo(rpc, owner, sesame)) + return RPCs.map(rpc => rpcToRpcinfo(rpc, owner, errorHandler, sesame)) .map(info => { const suffix = makeUnique?"-"+uuidv4().substr(0,4):"" const ret:any = info ret.uniqueName = info.name+suffix + let rpcFunction - switch(info.type){ - case "Hook": - socket.hook(ret.uniqueName, info.generator(socket)) - break; - case "Call": - socket.hook(ret.uniqueName, info.call) - break; - } - socket.on('close', () => socket.unhook(info.name)) + if(info.type === 'Hook') + rpcFunction = info.generator(socket) + else + rpcFunction = info.call + + socket.hook(ret.uniqueName, callGenerator(socket, rpcFunction, errorHandler)) return ret }) } + +/** + * Decorate an RPC with the error handler + * @param rpcFunction the function to decorate + */ +const callGenerator = (socket: I.Socket, rpcFunction : T.AnyFunction, errorHandler: T.ErrorHandler) : T.AnyFunction => { + const argsArr = extractArgs(rpcFunction) + const args = argsArr.join(',') + const argsStr = argsArr.map(stripAfterEquals).join(',') + + return eval(`async (`+args+`) => { + try{ + return await rpcFunction(`+argsStr+`) + }catch(e){ + errorHandler(socket)(e) + } + }`) +} + +/** + * Utility function to strip parameters like "a = 3" of their defaults + * @param str The parameter to modify + */ +export function stripAfterEquals(str:string):string{ + return str.split("=")[0] +} + /** * Utility function to generate {@link HookFunction} from a RPC for backend * @param rpc The RPC to transform * @returns A {@link HookFunction} */ -const hookGenerator = (rpc:T.HookRPC, sesameFn?: T.SesameFunction): T.HookInfo['generator'] => { +const hookGenerator = (rpc:T.HookRPC, errorHandler: T.ErrorHandler, sesameFn?: T.SesameFunction): T.HookInfo['generator'] => { const argsArr = extractArgs(rpc.hook) argsArr.pop() //remove 'callback' from the end const argsStr = argsArr.join(',') if(sesameFn){ const args = ['sesame', ...argsArr].join(',') - const f = eval(`(socket) => async (`+args+`) => { - if(!sesameFn(sesame)) return - const res = await rpc.hook(`+argsStr+(argsStr.length!==0?',':'')+` (...cbargs) => { + return eval(`(socket) => async (`+args+`) => { + try{ + if(!sesameFn(sesame)) return + const res = await rpc.hook(`+argsStr+(argsStr.length!==0?',':'')+` (...cbargs) => { + if(rpc.onCallback) rpc.onCallback.apply({}, cbargs) + socket.call.apply(socket, [res.uuid, ...cbargs]) + }) + return res + }catch(e){ + errorHandler(socket, e) + } + }`) + } + const args = argsArr.join(',') + return eval(`(socket) => async (`+args+`) => { + try{ + const res = await rpc.hook(`+args+(args.length!==0?',':'')+` (...cbargs) => { if(rpc.onCallback) rpc.onCallback.apply({}, cbargs) socket.call.apply(socket, [res.uuid, ...cbargs]) }) return res - }`) - return f - } - const args = argsArr.join(',') - return eval(`(socket) => async (`+args+`) => { - const res = await rpc.hook(`+args+(args.length!==0?',':'')+` (...cbargs) => { - if(rpc.onCallback) rpc.onCallback.apply({}, cbargs) - socket.call.apply(socket, [res.uuid, ...cbargs]) - }) - return res + }catch(e){ + errorHandler(socket, e) + } }`) } +const makeError = (callName: string) => { + return new Error("Unhandled Promise rejection: Call not found: "+callName+". ; Zone: ; Task: Promise.then ; Value: Error: Call not found: "+callName) +} /** * Extract a string list of parameters from a function diff --git a/test/Test.ts b/test/Test.ts index a4582e4..172c577 100644 --- a/test/Test.ts +++ b/test/Test.ts @@ -2,6 +2,8 @@ import { describe, it, Func } from "mocha"; import { RPCServer, RPCSocket, SubscriptionResponse, makeSubResponse } from '../Index' import * as uuidv4 from "uuid/v4" +import { doesNotReject } from "assert"; +import { Socket } from "dgram"; const add = (...args:number[]) => {return args.reduce((a,b)=>a+b, 0)} function makeServer(){ @@ -284,8 +286,23 @@ describe('Sesame should unlock the socket', () => { const sock = new RPCSocket(21004, "localhost") sock.connect( /* no sesame */).then(async (c) => { c.test.checkCandy().then(d => { - if(d === candy) - done("should not be able to get candy") + done() + }).catch(e => { + //console.log("EXPECTED CLIENT EXCEPTION", String(e)); + done() + }).finally(() => { + sock.destroy() + }) + }) + }) + + it('should fail with wrong sesame', (done) => { + const sock = new RPCSocket(21004, "localhost") + sock.connect('abasd').then(async (c) => { + c.test.checkCandy().then(d => { + done("should not be able to get candy") + }).catch(e => { + //console.log("EXPECTED CLIENT EXCEPTION", String(e)); done() }).finally(() => { sock.destroy() @@ -300,7 +317,7 @@ describe('Sesame should unlock the socket', () => { } }).then(d => { if(d.result !== 'Success') - done('expected valid response') + done('unexpected valid response') client.test.checkCandy() }) @@ -326,42 +343,76 @@ describe('Sesame should unlock the socket', () => { }) }) -/* -class myServer{ - server = new RPCServer(21004, [ { - name: 'createUser' as 'createUser', - exportRPCs: () => [{ - name: 'createUser' as 'createUser', - call: this.createUser - }] - } - ]) - createUser = async( user: {a:any,b:any}) => { - console.log(user) - return user - } -} -describe('Should pass the createUser edge case', ()=>{ - let server - - before(()=>{ - server = new myServer() - }) - - after(()=>{ - server.server.destroy() - }) - - it("should work", async ()=>{ - let sock = new RPCSocket(21004, 'localhost') - let client = await sock.connect() - client["createUser"]["createUser"]({ - a:'a', - b:'b' - }).then(console.log) - }) +describe('Error handling', ()=>{ + let createUser = async( user: {a:any,b:any}) => { + throw new Error("BAD BAD BAD") + } + + it("RPC throws on client without handler", (done)=>{ + let server = new RPCServer(21004, [ { + name: 'createUser' as 'createUser', + exportRPCs: () => [{ + name: 'createUser' as 'createUser', + call: createUser + }]}], { + + }) + + let sock = new RPCSocket(21004, 'localhost') + sock.connect().then((cli) => { + cli["createUser"]["createUser"]({ + a:'a', + b:'b' + }) + .then(r => { + if(r != null) + done("UNEXPECTED RESULT " + r) + }) + .catch((e) => { + //console.log("EXPECTED CLIENT EXCEPTION", String(e)); + done() + }) + .finally(() => { + sock.destroy() + server.destroy() + }) + }) + }) + + it("RPC throws server with handler", (done)=>{ + let server = new RPCServer(21004, [ { + name: 'createUser' as 'createUser', + exportRPCs: () => [{ + name: 'createUser' as 'createUser', + call: createUser + }]}], { + errorHandler: (socket, e) => { + //console.log("EXPECTED SERVER EXCEPTION", String(e)); + done() + } + }) + + let sock = new RPCSocket(21004, 'localhost') + sock.connect().then((cli) => { + cli["createUser"]["createUser"]({ + a:'a', + b:'b' + }) + .then(r => { + if(r != null) + done("UNEXPECTED RESULT " + r) + }) + .catch((e) => { + done("UNEXPECTED CLIENT ERROR " + e) + done(e) + }) + .finally(() => { + sock.destroy() + server.destroy() + }) + }) + }) }) -*/ \ No newline at end of file