Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Customizable Error Filtering for js-rpc through User-Defined toError/fromError Functions #10

Closed
8 tasks done
addievo opened this issue Sep 18, 2023 · 22 comments · Fixed by #8 or #34
Closed
8 tasks done

Customizable Error Filtering for js-rpc through User-Defined toError/fromError Functions #10

addievo opened this issue Sep 18, 2023 · 22 comments · Fixed by #8 or #34
Assignees
Labels
development Standard development r&d:polykey:supporting activity Supporting core activity
Milestone

Comments

@addievo
Copy link
Contributor

addievo commented Sep 18, 2023

Description

Enable user-defined error serialization and deserialization through toError and fromError functions in js-rpc. Add optional replacer and reviver parameters to further customize the (de)serialization process, thereby enhancing security by allowing application-specific rules for sensitive data filtering.

Requirements

  1. User-Driven: User should be able to define toError and fromError functions.
  2. Optional Parameters: Introduce replacer and reviver for added customization.
  3. Application-Specific: Allow injectable rules for data filtering.
  4. PK Conformance: Compatibility with existing Polykey workflows.

Tasks

  • Parameterize toError and fromError: Accept user-defined implementations.
  • Constructor Implementation: Include toError, fromError, replacer, and reviver in the constructor.
  • Unit Testing: Create tests for the new features.
  • Documentation: Update js-rpc documentation.

Acceptance Criteria

  • User-defined toError and fromError functions can be successfully implemented.
  • Optional replacer and reviver parameters function as expected.
  • The system supports application-specific data filtering rules.
  • Documentation is updated.
@addievo addievo added the development Standard development label Sep 18, 2023
@addievo addievo added this to the testnet7 milestone Sep 18, 2023
@addievo addievo self-assigned this Sep 18, 2023
@addievo addievo mentioned this issue Sep 18, 2023
31 tasks
@CMCDragonkai
Copy link
Member

This could be have been in #3.

@addievo
Copy link
Contributor Author

addievo commented Sep 20, 2023

Untitled-2023-08-24-1311

@addievo
Copy link
Contributor Author

addievo commented Sep 20, 2023

Updated diagram, removing sensitiveReplacer, no longer required.
Untitled-2023-08-24-1311 excalidraw

@CMCDragonkai
Copy link
Member

A single replacer should be sufficient.

What does the API look like?

@addievo
Copy link
Contributor Author

addievo commented Sep 21, 2023

A single replacer should be sufficient.

What does the API look like?

RIght, sensitive replacer was related stack value of PK. So I will refactor that out.

API of?

@CMCDragonkai
Copy link
Member

Give me an example of how it would be used here.

@addievo
Copy link
Contributor Author

addievo commented Sep 21, 2023

JSON RPC reponse :

{
"jsonrpc": "2.0", 
"error": {
    "code": -32700,
     "message": "Parse error",
     "data?": "Some data",
     "type": ErrorRPCParse,
     }, 
"id": null
}

fromError should return JSONValue object.

replacer is now a parameter too, which works on JSONValue passed by fromError.

replacer should be augemented to be able to extract error from JSON,
the application replacer should work on the library replacer,
since replacer acts on data within an error, i.e. on

"error": {
    "code": -32700,
     "message": "Parse error",
     "data?": "Some data",
     "type": ErrorRPCParse,
     }, 

toError does not necessarily need a reviver as a parameter.

@addievo
Copy link
Contributor Author

addievo commented Sep 21, 2023

fromError

  1. It should now return a JSONValue object instead of a serialized JSON string. This makes it more flexible and allows for further manipulation if needed.

Updated fromError Function - draft

type JSONValue = string | number | boolean | null | { [key: string]: JSONValue } | JSONValue[];
function fromError(error: ErrorRPC<any>, sensitive: boolean = false): JSONValue {
  return {
    type: error.name,
    data: {
      message: error.message,
      code: error.code,
      description: error.description,
    },
  };
}

replacer

  1. It should work on the JSONValue object returned by fromError.
  2. It should be able to extract data from the error JSON object.

Updated replacer Function - draft

function replacer(key: string, value: any): any {
  if (value instanceof ErrorRPC) {
    return {
      code: value.code,
      message: value.description, 
      "data?": value.someData, 
      type: value.constructor.name, 
    };
  }
  return value;
}

toError

  1. Should not necessarily need a reviver as a parameter.

Updated toError Function

function toError(errorData: any, metadata?: JSONValue): Error {
  const error = new Error(); // or any custom error class
  error.message = errorData.message || '';
  // Other custom operations can be performed here
  return error;
}

@CMCDragonkai
Copy link
Member

The replacer function might need to be a bit more sophisticated. The replacer actually runs within the error property of the top level JSON object. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#the_replacer_parameter

You'll need to maintain a sort of context to know if you are under the tree of error property. It's basically a fragment cursor iterating over the tree (graph). So you just don't want to run it on anything other than the error property.

Given that code and message are mandatory properties... you can apply it to any property under error except code and message I reckon.

image

addievo added a commit that referenced this issue Sep 22, 2023
* Related #4

fix: parameterize replacer toError and fromError, change fromError to return JSONValue, stringify fromError usages

* Related #10
addievo added a commit that referenced this issue Sep 22, 2023
addievo added a commit that referenced this issue Sep 26, 2023
addievo added a commit that referenced this issue Sep 26, 2023
addievo added a commit that referenced this issue Sep 26, 2023
callers and handlers are now refactored

* WIP - Newline now works, refers issue #1

node v20 fix

feat: handlers implementations are now abstract arrow functions

* Fixes #5

[ci skip]

* resolves issue 5, makes RPC handlers abstract arrow function properties

feat: rename to uppercase

[ci skip]

fix: handler export fix

[ci skip]

fix: tsconf from quic

[ci skip]

fix: dependencies (js quic), events and errors versions, changing to relative imports, jest dev dependency, js-quic tsconfig

[ci skip]

fix: tests imports, using @

[ci skip]

chore: removed sysexits

chore: fix default exports for callers and handlers
Fixed index for handlers

fix: remove @matrixai/id

fix: remove @matrixai/id and ix

chore : diagram

[ci skip]

chore : lintfix
fix: errors now extend AbstractError

[ci skip]

fix: undoing fix #1

[ci skip]

replacd errorCode with just code, references std error codes from rpc spec

feat: events based createDestroy

[ci skip]

chore: img format fix

[ci skip]

chore: img in README.md

[ci skip]

feat: allows the user to pass in a generator function if the user wishes to specify a particular id

[ci skip]

fix: fixes #7

* Removes graceTimer and related jests

chore: idGen name change. idGen parameter in creation and constructor. No longer optional. Only defaulted in one place.

wip: added idgen to jests, was missing.

[ci skip]

wip: reimported ix, since a few tests rely on it.
removed, matrixai/id

wip: jests for #4
removed, matrixai/id

wip: * Implements custom RPC Error codes.
     * Fixed jest for concurrent timeouts
     * All errors now have a cause
     * All errors now use custom error codes.

wip: *Client uses ctx timer now

wip: *Jests to test concurrency

wip: *custom RPC based errors for RPC Client, now all errors have a cause and an error message

WIP: * Refactor out sensitiveReplacer

WIP: * Refactor out sensitiveReplacer

WIP: * Update to latest async init and events
* set default timeout to Infinity
* jest to check server and client with infinite timeout
* fixing jests which broke after changing default timeout to infinity

WIP: f1x #4

WIP: f1x #11

f1x: parameterize toError, fromError and replacer

wip: tofrom

fix: parameterize toError, fromError and replacer

fix: Makes concurrent jests non deterministic

* Related #4

fix: parameterize replacer toError and fromError, change fromError to return JSONValue, stringify fromError usages

* Related #10

fix: Converted global state for fromError to handle it internally.

*Related: #10
Reviewed-by: @tegefaulkes
[ci skip]

chore: Jests for fromError and toError, and using a custom replacer.

related: #10

[ci skip]
addievo added a commit that referenced this issue Sep 26, 2023
@CMCDragonkai
Copy link
Member

Reopening this because RPCServer.createRPCServer looks incorrect.

image

@CMCDragonkai
Copy link
Member

Errors can go both directions. Server to client and client to server. This means both RPCClient and RPCServerneedsfromErrorandtoError`.

On top of this, sensitive doesn't make sense to me. The existence of a specialised replacer function is basically intended to act as the sensitive remover. Why would there be a separate sensitive boolean? There'd be no use for this at all.

Furthermore // 1 minute is wrong. Remove spurious comments like this.

@CMCDragonkai
Copy link
Member

Another issue is the idea of the default middleware. Is the default middleware necessary? If so, you cannot expect the user to supply middleware combined with the default if they are not aware of it.

You must then instead take in additional middleware and compose with the default middleware.

I'm not sure about this but @tegefaulkes can explain more later about this.

@CMCDragonkai
Copy link
Member

Therefore the RPCServer API should look like this (not including the middleware parameters atm):

await RPCServer.createRPCServer({
  manifest,
  fromError,
  toError,
  filterSensitive
});

@CMCDragonkai CMCDragonkai reopened this Sep 27, 2023
@tegefaulkes
Copy link
Contributor

Default middleware is a compromise. We need to map the stream from UInt8Array to the JSONRPCMessages. If we wanted to allow the user to supply middleware that could work on the UInt8Array part of the stream such as applying compression/encryption. Then we needed to expose that to them. But the RPC requires some mapping from the raw stream to the RPC messages. This is the default middleware's job.

The alternative was to split up the user supplied middleware into stages. raw data stage and JSON message stage. To do this it will take a little bit of refactoring and prototyping with how the middleware is supplied and composed.

@CMCDragonkai
Copy link
Member

Let's examine the relationship between JSON RPC errors and stream errors.

On the transport layer we have stream error codes. This is why we have codeToReason and reasonToCode functions. They are used by both sides of the duplex stream to deal convert a stream error code to a reason any type. The reason can be an any type, because this is accepted by readable.cancel(reason) and writable.abort(reason). This is what we call "transport-level stream errors". (There's also transport-level connection errors, but that's a separate thing).

On the application layer, we have RPC errors. In this case an error is encoded as per the JSON RPC protocol. https://www.jsonrpc.org/specification#error_object

Which might look something like this:

{"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "1"}

Therefore let's consider how toError and fromError works.

Server Side

The idea here is that during the handling of a stream. The handler function may throw up an error. Let's consider just for unary handlers.

handler() {
  throw new SpecialError('oh no');
}

In this case the fromError is applied to transform SpecialError to a specific serialisation of that SpecialError that should look like:

{"code": -32601, "message": "Method not found"}

This could be done by default to be:

(e: Error) => {
  return {
    code: e.code ?? utils.rpcDefaultCode,
    message: e.message
  };
};

The result is then combined with the rest of the JSON RPC message to eventually produce:

{
  "jsonrpc": "2.0", 
  "error": {
    "code": -32000, 
    "message": "oh no"
  }, 
  "id": null
}

On the other side, when it receives this JSON RPC error message. The parser/middleware is supposed to understand it, and thus convert it back to the exception object, that being of toError. The default of which is simply:

({ code, message }) => {
  return new Error(`${code.toString()} ${message.toString()}`),
};

This is actually similar to the default we have in QUICStream for codeToReason. However there's some discussion to change this to a ErrorQUICStreamReadable or ErrorQUICStreamWritable sort of thing for generic errors on the stream.

For the client, it now as per calling the networked handler, should throw up that exception object.

try {
  await rpcClient.doThisSpecialThing();
} catch (e) {
  console.log(e.message); // -32000 oh no
}

Client

For the client the opposite order occurs. But it still requires toError and fromError the same.

For Streams

Streams needs to do the same thing. Stream handlers have to now deal with errors being thrown into the async iterator/generator and dealt with too.

@CMCDragonkai
Copy link
Member

Some discussion with @tegefaulkes.

  • toError and fromError needs to be on both client and server.
  • For unary and server streaming, the client side would not use fromError and the server side would not use toError because after sending the initial message, you cannot send any more messages from the client to server. You would just do p.cancel which can only result in a transport-level stream error code.
  • For client streaming and duplex streaming, the client side would be using fromError and the server side would be using toError too, because they can actually throw an error into the stream.

Regardless, when creating RPCClient and RPCServer, you must able to inject both into them, because they may be utilised depending on the type of the handlers involved.

@CMCDragonkai
Copy link
Member

Default middleware is a compromise. We need to map the stream from UInt8Array to the JSONRPCMessages. If we wanted to allow the user to supply middleware that could work on the UInt8Array part of the stream such as applying compression/encryption. Then we needed to expose that to them. But the RPC requires some mapping from the raw stream to the RPC messages. This is the default middleware's job.

The alternative was to split up the user supplied middleware into stages. raw data stage and JSON message stage. To do this it will take a little bit of refactoring and prototyping with how the middleware is supplied and composed.

So for now, if the user wants to supply custom middleware, they have to combine it with the default middleware explicitly? Is this guaranteed by the types?

@tegefaulkes
Copy link
Contributor

Using the default middleware isn't enforced by types. But that mapping of Uint8Array -> JSONRPCMessages is.

@tegefaulkes
Copy link
Contributor

Seems that errors sent through the forward path just wasn't supported and I think I know why.

The JSONRPC spec doesn't really allow it. The Error message is a response type message. So it's not really allowed

/**
 * This is the JSON RPC Request object. It can be a request message or
 * notification.
 */
type JSONRPCRequest<T extends JSONValue = JSONValue> =
  | JSONRPCRequestMessage<T>
  | JSONRPCRequestNotification<T>;

/**
 * This is a JSON RPC response object. It can be a response result or error.
 */
type JSONRPCResponse<T extends JSONValue = JSONValue> =
  | JSONRPCResponseResult<T>
  | JSONRPCResponseError;

I'm not sure it's a simple change to allow RPC errors on the forward path and even if we add it its not really a feature we need.

@addievo
Copy link
Contributor Author

addievo commented Sep 28, 2023

Therefore the RPCServer API should look like this (not including the middleware parameters atm):

await RPCServer.createRPCServer({
  manifest,
  fromError,
  toError,
  filterSensitive
});

whats filter sensitive for? The replacer can take in any key as a parameter, definable by the user

@addievo
Copy link
Contributor Author

addievo commented Sep 28, 2023

@CMCDragonkai, client doesn't currently transmit any errors to server, so whats exactly the need for fromError in client and conversely toError in server?

@addievo
Copy link
Contributor Author

addievo commented Sep 28, 2023

Moved to #18

@addievo addievo closed this as completed Sep 28, 2023
addievo pushed a commit that referenced this issue Oct 9, 2023
# This is the 1st commit message:

fix: RPCServer.start is now no longer a factory function

fix: fixed RPC tests after async-init change

# This is the commit message #2:

chore: updated inline documentation according to async-init changes

# This is the commit message #3:

wip: fixing imports

# This is the commit message #4:

wip: RPCServer deconstruction

# This is the commit message #5:

fix: RPCServer start stop order

# This is the commit message #6:

fix: added back createDestroy back to RPCClient

# This is the commit message #7:

chore: lintfix

# This is the commit message #8:

wip: completely removed create destroy from rpcclient

# This is the commit message #9:

fix: RPCClient tests after async-init changes

# This is the commit message #10:

wip

# This is the commit message #11:

fix: idgen is optional in constructor

# This is the commit message #12:

fix: type import in ./types

# This is the commit message #13:

chore: test for RPCServer force stopping

# This is the commit message #14:

fix: proper implementation of fromError toError, clientOutputTransformStream no longer outputs error.data, but rather error directly

# This is the commit message #15:

fix: jest fix for ErrorRPCRemote

# This is the commit message #16:

wip toError fromError

fix: proper implementation of fromError toError, clientOutputTransformStream no longer outputs error.data, but rather error directly

fix: changed rpcClient toError implementation

fix: changing ErrorRPCRemote constructor to be in-line with toError error creation constructor

fix: jest fix for ErrorRPCRemote

fix: fixing exports

fix: RPC errors now correctly extend AbstractError

fix: removed old events
fix: cleaned up imports
feat: client has toError as parameter
fix: removed type from JSONError obj
fix: startStop constructor correctly used
fix: infinity is definitely not == 1 minute

chore:  rename variable
@CMCDragonkai CMCDragonkai added the r&d:polykey:supporting activity Supporting core activity label Aug 13, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
development Standard development r&d:polykey:supporting activity Supporting core activity
Development

Successfully merging a pull request may close this issue.

3 participants