Skip to content

Commit

Permalink
fix: Errors not visible when opening deephaven.ui widget
Browse files Browse the repository at this point in the history
- Add an error listener to a command
- WIP, does not actually display the error yet but is wired up to listen to it.
  - Need to pass the error down to `DocumentHandler`? Or have a separate `DocumentErrorHandler` widget?
  • Loading branch information
mofojed committed Apr 19, 2024
1 parent 802361f commit 4c4b08e
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 3 deletions.
38 changes: 38 additions & 0 deletions plugins/ui/src/deephaven/ui/object_types/ElementMessageStream.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from ..elements import Element
from ..renderer import NodeEncoder, Renderer, RenderedNode
from .._internal import RenderContext, StateUpdateCallable, ExportedRenderState
from .ErrorCode import ErrorCode

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -159,6 +160,9 @@ def _render(self) -> None:
self._send_document_update(node, state)
except Exception as e:
logger.exception("Error rendering %s", self._element.name)
# Try and send the error to the client
# There's a possibility this will error out too, but we can't do much about that
self._send_document_error(e)
raise e

def _process_callable_queue(self) -> None:
Expand Down Expand Up @@ -305,6 +309,29 @@ def _make_request(self, method: str, *params: Any) -> dict[str, Any]:
"id": self._get_next_message_id(),
}

def _make_error(
self,
message: str,
code: ErrorCode = ErrorCode.UNKNOWN,
data: Any = None,
) -> dict[str, Any]:
"""
Make a JSON-RPC error message.
Args:
message: The short error message description
code: The error code
data: Additional data
"""
return {
"jsonrpc": "2.0",
"error": {
"code": code,
"message": message,
"data": data,
},
}

def _make_dispatcher(self) -> Dispatcher:
dispatcher = Dispatcher()
dispatcher["setState"] = self._set_state
Expand Down Expand Up @@ -353,3 +380,14 @@ def _send_document_update(
dispatcher[callable_id] = wrap_callable(callable)
self._dispatcher = dispatcher
self._connection.on_data(payload.encode(), new_objects)

def _send_document_error(self, error: Exception) -> None:
"""
Send an error to the client. This is called when an error occurs during rendering.
Args:
error: The error that occurred
"""
request = self._make_error(str(error), ErrorCode.RENDER_ERROR)
payload = json.dumps(request)
self._connection.on_data(payload.encode(), [])
20 changes: 20 additions & 0 deletions plugins/ui/src/deephaven/ui/object_types/ErrorCode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from enum import Enum


class ErrorCode(int, Enum):
"""
ServerErrorCode is a list of error codes that can be returned by the server. Values are based on the JSON-RPC 2.0
specification. See https://www.jsonrpc.org/specification#error_object for more information.
The range -32000 to -32099 are reserved for implementation-defined server-errors.
"""

# General errors
UNKNOWN = -32600
"""
An unknown error occurred on the server.
"""

RENDER_ERROR = -32601
"""
There was an error when rendering the document.
"""
27 changes: 27 additions & 0 deletions plugins/ui/src/js/src/widget/JSONRPCUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { JSONRPCErrorResponse, isJSONRPCResponse } from "json-rpc-2.0";

export function isJSONRPCErrorResponse(obj: unknown): obj is JSONRPCErrorResponse {
return obj != null && isJSONRPCResponse(obj) && obj.error !== undefined;
}

/**
* Parse the error payload to get a user-friendly error message.
* @param error Error payload to parse. Should be a JSON-RPC error response object, an Error object, or a string.
* @returns A string of the error message to display to the user.
*/
export function parseServerErrorPayload(error: unknown): string {
if (isJSONRPCErrorResponse(error)) {
return error.error.message;
}

if (typeof error === 'string') {
return error;
}

if (error instanceof Error) {
return error.message;
}


return 'An unknown error occurred.';
}
34 changes: 31 additions & 3 deletions plugins/ui/src/js/src/widget/WidgetHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
} from './WidgetTypes';
import DocumentHandler from './DocumentHandler';
import { getComponentForElement } from './WidgetUtils';
import { parseServerErrorPayload } from './JSONRPCUtils';

const log = Log.module('@deephaven/js-plugin-ui/WidgetHandler');

Expand All @@ -52,6 +53,12 @@ export interface WidgetHandlerProps {
onDataChange?: (data: WidgetDataUpdate) => void;
}

type JsonRPCError = {
code: number;
message: string;

}

function WidgetHandler({
onClose,
onDataChange = EMPTY_FUNCTION,
Expand All @@ -62,6 +69,7 @@ function WidgetHandler({
const [widget, setWidget] = useState<dh.Widget>();
const [document, setDocument] = useState<ReactNode>();
const [initialData] = useState(initialDataProp);
const [error, setError] = useState<string>();

// When we fetch a widget, the client is then responsible for the exported objects.
// These objects could stay alive even after the widget is closed if we wanted to,
Expand All @@ -80,7 +88,18 @@ function WidgetHandler({
new JSONRPCClient(request => {
log.debug('Sending request', request);
widget.sendMessage(JSON.stringify(request), []);
})
}),
{
/**
*
* @param message It just says an invalid JSON-RPC response was received
* @param payload The payload of the error message. This is the JSON-RPC response that was received.
*/
errorListener: (message, payload) => {
log.warn('JSONRPCServer/Client error', message, payload);
setError(parseServerErrorPayload(payload));
},
}
)
: null,
[widget]
Expand Down Expand Up @@ -220,14 +239,20 @@ function WidgetHandler({

// Set a var to the client that we know will not be null in the closure below
const activeClient = jsonClient;
function receiveData(
async function receiveData(
data: string,
newExportedObjects: dh.WidgetExportedObject[]
) {
log.debug2('Data received', data, newExportedObjects);
updateExportedObjects(newExportedObjects);
if (data.length > 0) {
activeClient.receiveAndSend(JSON.parse(data));
try {
await activeClient.receiveAndSend(JSON.parse(data));
} catch (e) {
// We already have an `errorListener` registered when declaring the JSONRPCServerAndClient,
// and that contains more information than this error does, so just use that.
log.debug('Error receiving data', e);
}
}
}

Expand Down Expand Up @@ -299,6 +324,9 @@ function WidgetHandler({
[fetch, descriptor]
);

// TODO: If there's an error, we should display it in a panel to the user
// Some sort of DocumentErrorHandler? Just displays an error message in all known `panelIds`, or opens a new `panelId`?

return useMemo(
() =>
document != null ? (
Expand Down

0 comments on commit 4c4b08e

Please sign in to comment.