Skip to content

Conversation

imanjra
Copy link
Contributor

@imanjra imanjra commented Oct 15, 2025

What changes are proposed in this pull request?

improve error details for when an operator returns non-serializable value

How is this patch tested? If it is not, please explain why.

Using a test operator and python panel event

Release Notes

Is this a user-facing change that should be mentioned in the release notes?

  • No. You can skip the rest of this section.
  • Yes. Give a description of this change to be included in the release
    notes for FiftyOne users.

improve error details for when an operator returns non-serializable value

Todo

Update docs to note the rule that operator, python-panel lifecycle methods, and python-panel events must return JSON-serializable values. If a complex value to be returned (i.e. DatasetView), it must be serialized or parsed into serialiazble object

What areas of FiftyOne does this PR affect?

  • App: FiftyOne application changes
  • Build: Build and test infrastructure changes
  • Core: Core fiftyone Python library changes
  • Documentation: FiftyOne documentation changes
  • Other

Summary by CodeRabbit

  • Bug Fixes

    • Error panel now shows a distinct "Trace" section and includes explicit message entries for operator errors.
    • Operator output displays more informative, context-aware error reasons.
    • Prevented empty output views from being rendered when there are no results.
  • Refactor

    • Unified JSON response creation for more consistent API responses.
    • Improved numeric-type serialization for reliability.
    • Enhanced server error responses to include structured messages and stack traces.

@imanjra imanjra requested review from a team as code owners October 15, 2025 19:09
@imanjra imanjra requested a review from ritch October 15, 2025 19:09
Copy link
Contributor

coderabbitai bot commented Oct 15, 2025

Walkthrough

Adds a shared JSON response encoder and async create_response in fiftyone.core.utils, replaces local response builders to use it, and introduces create_operator_response to serialize ExecutionResult with structured error handling. Also refines ErrorBoundary operator error labeling and OperatorPromptOutput early-return and reason derivation.

Changes

Cohort / File(s) Summary
Error display tweak
app/packages/components/src/components/ErrorBoundary/ErrorBoundary.tsx
For OperatorError, conditionally adds a "Message" entry when error.message exists; ensures the stack trace is emitted as a separate "Trace" entry and removes the prior unconditional stack entry labeled by error.message.
Operator prompt output guard & reason
app/packages/operators/src/components/OperatorPromptOutput.tsx
Adds an early return when no outputFields and no executor/resolve errors; computes reason from error.bodyResponse.error with a fallback default message; uses that computed reason in ErrorView while preserving error-detail formatting and normal OperatorIO rendering.
Core JSON response utilities
fiftyone/core/utils.py
Adds Encoder (subclass of JSONEncoder) to handle NumPy scalar types; adds async create_response(response: dict) that builds a JSON HTTP response using the encoder (via background sync) and returns a Starlette Response; introduces a lazy fos singleton for fiftyone.core.storage.
Operator response creation utility
fiftyone/operators/utils.py
Adds async def create_operator_response(result: ExecutionResult) -> Union[Response, JSONResponse] which serializes an ExecutionResult via core create_response; re-raises HTTPException; logs and returns structured 500 JSON on serialization errors (including stack trace).
Operator execution response path
fiftyone/operators/server.py
In ExecuteOperator.post, replaces the direct result.to_json() with await create_operator_response(result) so the endpoint returns the Response produced by the new utility.
Server decorators use shared response util
fiftyone/server/decorators.py
Removes local Encoder and local create_response; imports and delegates to fiftyone.core.utils.create_response for route responses and simplifies imports and response construction accordingly.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant Server as Server Route
  participant Exec as Operator Executor
  participant OpUtils as operators.utils<br/>create_operator_response
  participant Core as core.utils<br/>create_response
  participant Starlette as Starlette Response

  Client->>Server: POST /operators/execute
  Server->>Exec: execute()
  Exec-->>Server: ExecutionResult
  Server->>OpUtils: create_operator_response(result)
  alt Success
    OpUtils->>Core: create_response(result.to_dict())
    Core-->>OpUtils: JSON Response
    OpUtils-->>Server: Response
    Server-->>Client: 200 JSON
  else HTTPException
    OpUtils-->>Server: re-raise
    Server-->>Client: HTTP error
  else Serialization error
    OpUtils->>OpUtils: log error + traceback
    OpUtils-->>Server: 500 JSON {"error":"server-error","trace":...}
    Server-->>Client: 500 JSON
  end
Loading
sequenceDiagram
  autonumber
  actor User
  participant UI as OperatorPromptOutput

  alt No outputFields and no errors
    UI-->>User: Render nothing (early return)
  else Error present
    UI->>UI: reason = error.bodyResponse.error || default
    UI-->>User: Render ErrorView(reason, details)
  else Has outputFields
    UI-->>User: Render OperatorIO
  end
Loading
sequenceDiagram
  autonumber
  participant App as ErrorBoundary
  participant View as ErrorDisplayMarkup

  Note over App,View: OperatorError display labeling change
  App->>View: pass OperatorError
  View->>View: if error.message -> add "Message" entry
  View->>View: add "Trace" entry for stack
  View-->>App: Render entries
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

I nibble bytes with whiskered cheer,
Encoders hum and responses clear.
Traces labeled, reasons found,
Prompts return or stay unbound.
Hop—these changes make the paths appear. 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.56% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The pull request title clearly and concisely summarizes the main change by indicating that error details for operators returning non-serializable values are improved, and it directly relates to the core change in the diff without extraneous information.
Description Check ✅ Passed The pull request description follows the repository template by including the proposed changes, testing procedure, release notes with proper user-facing indication, and affected areas, providing sufficient context for reviewers.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bugfix/panel-event-return

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
fiftyone/core/utils.py (1)

3197-3208: Consider expanding numpy type coverage.

The Encoder currently only handles np.floating and np.integer. If operators can return other numpy types (e.g., np.ndarray, np.bool_, np.str_), they will fail serialization.

Consider extending the encoder to handle additional numpy types:

 class Encoder(JSONEncoder):
     """Custom JSON encoder that handles numpy types."""
 
     def default(self, o):
         if isinstance(o, np.floating):
             return float(o)
 
         if isinstance(o, np.integer):
             return int(o)
+
+        if isinstance(o, np.bool_):
+            return bool(o)
+
+        if isinstance(o, np.ndarray):
+            return o.tolist()
 
         return JSONEncoder.default(self, o)
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 126520c and b937771.

📒 Files selected for processing (6)
  • app/packages/components/src/components/ErrorBoundary/ErrorBoundary.tsx (1 hunks)
  • app/packages/operators/src/components/OperatorPromptOutput.tsx (2 hunks)
  • fiftyone/core/utils.py (2 hunks)
  • fiftyone/operators/server.py (2 hunks)
  • fiftyone/operators/utils.py (2 hunks)
  • fiftyone/server/decorators.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

⚙️ CodeRabbit configuration file

Review the Typescript and React code for conformity with best practices in React, Recoil, Graphql, and Typescript. Highlight any deviations.

Files:

  • app/packages/operators/src/components/OperatorPromptOutput.tsx
  • app/packages/components/src/components/ErrorBoundary/ErrorBoundary.tsx
🔇 Additional comments (6)
app/packages/components/src/components/ErrorBoundary/ErrorBoundary.tsx (1)

75-86: LGTM! Improved error display for OperatorError.

The conditional check for error.message prevents rendering an empty Message section, and the consistent "Trace" label improves clarity.

app/packages/operators/src/components/OperatorPromptOutput.tsx (1)

11-16: LGTM! Dynamic error messaging improves user experience.

The early return avoids unnecessary rendering, and the dynamic reason provides more context from server responses. The chaining error?.bodyResponse?.error assumes the server provides this structure, which aligns with the centralized response creation in fiftyone/operators/utils.py.

fiftyone/core/utils.py (1)

3210-3216: LGTM! Centralized response creation improves maintainability.

The create_response function properly delegates JSON encoding to a background task using run_sync_task, which is appropriate for async contexts.

fiftyone/operators/server.py (1)

110-110: LGTM! Improved error handling for operator responses.

Delegating to create_operator_response centralizes JSON serialization and provides better error messages when operators return non-serializable values, which is the objective of this PR.

fiftyone/server/decorators.py (1)

19-19: LGTM! Eliminates code duplication.

Using the centralized create_response from fiftyone.core.utils removes duplicate encoding logic and ensures consistent JSON serialization across the codebase.

fiftyone/operators/utils.py (1)

119-156: LGTM! Comprehensive error handling with helpful developer guidance.

The create_operator_response function provides excellent error handling and a clear message when serialization fails: "Make sure that the return value of the operation is JSON-serializable." This directly addresses the PR objective.

Note: The type hint Response | JSONResponse uses Python 3.10+ syntax. Ensure the project targets Python 3.10 or later, or use Union[Response, JSONResponse] for compatibility.

@imanjra imanjra force-pushed the bugfix/panel-event-return branch from b937771 to 850f874 Compare October 15, 2025 19:25
jleven
jleven previously approved these changes Oct 15, 2025
Copy link
Contributor

@jleven jleven left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me!!

You may want a second opinion, though.

ritch
ritch previously approved these changes Oct 15, 2025
)


class Encoder(JSONEncoder):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does it make sense to move this out of server/? seems very server specific

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved it out to core.utils as it is now also used by create_operator_response


msg = (
f"Failed to serialize operator result. {e}."
+ " Make sure that the return value of the operation is JSON-serializable."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would users know that dataset and view are not serializable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessarily, but with stack trace and this message, it should give them pointers

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is enough for the release. Always room to improve moving forward.


if isinstance(o, np.integer):
return int(o)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haven't thought it through completely, but why wouldn't we just handle the serialization here? eg if isinstance(o, DatasetView): return o._serialize()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yah, I consider customizing but I think we should leave serialization of complex values to be case-by-case. For example, what if they have view in a nested object or what if another complex class is returned

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that this work is release blocking, it's helpful to keep the change as minimal and safe as possible

@imanjra imanjra dismissed stale reviews from ritch and jleven via e716408 October 16, 2025 19:31
@imanjra imanjra force-pushed the bugfix/panel-event-return branch from 850f874 to e716408 Compare October 16, 2025 19:31
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
app/packages/components/src/components/ErrorBoundary/ErrorBoundary.tsx (1)

85-85: Add defensive check for error.stack existence.

The trace entry is added unconditionally for OperatorError without checking if error.stack exists, which is inconsistent with the handling of other error types on lines 87-88. This could result in displaying undefined content.

Apply this diff to add a defensive check:

-    messages.push({ message: "Trace", content: error.stack });
+    if (error.stack) {
+      messages.push({ message: "Trace", content: error.stack });
+    }
fiftyone/core/utils.py (1)

3210-3215: Consider documenting the parameter in the docstring.

The function correctly delegates serialization to a background thread and returns a properly formatted JSON response. However, the docstring could be improved.

Apply this diff to enhance the docstring:

 async def create_response(response: dict):
-    """Creates a JSON response from the given dictionary."""
+    """Creates a JSON response from the given dictionary.
+    
+    Args:
+        response: a dictionary to serialize as JSON
+        
+    Returns:
+        a Starlette Response with JSON content
+    """
     return Response(
         await run_sync_task(lambda: json_util.dumps(response, cls=Encoder)),
         headers={"Content-Type": "application/json"},
     )
fiftyone/operators/utils.py (1)

119-156: Excellent error handling for operator serialization.

The function provides structured error handling that clearly communicates serialization failures to users. The error message appropriately guides users to ensure return values are JSON-serializable, which addresses the PR's objective of improving error details.

Minor note: The error response includes both "error" and "message" fields with identical content (lines 150-152). This redundancy is likely for backward compatibility, but if not required, you could remove one field.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 850f874 and e716408.

📒 Files selected for processing (6)
  • app/packages/components/src/components/ErrorBoundary/ErrorBoundary.tsx (1 hunks)
  • app/packages/operators/src/components/OperatorPromptOutput.tsx (2 hunks)
  • fiftyone/core/utils.py (2 hunks)
  • fiftyone/operators/server.py (2 hunks)
  • fiftyone/operators/utils.py (2 hunks)
  • fiftyone/server/decorators.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • fiftyone/operators/server.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

⚙️ CodeRabbit configuration file

Review the Typescript and React code for conformity with best practices in React, Recoil, Graphql, and Typescript. Highlight any deviations.

Files:

  • app/packages/operators/src/components/OperatorPromptOutput.tsx
  • app/packages/components/src/components/ErrorBoundary/ErrorBoundary.tsx
🔇 Additional comments (7)
app/packages/operators/src/components/OperatorPromptOutput.tsx (3)

11-11: LGTM! Early return improves efficiency.

The early return when there are no outputFields and no errors is appropriate and improves performance.


14-16: LGTM! Dynamic error message construction improves error reporting.

The extraction of the error message from error?.bodyResponse?.error and dynamic construction of the reason string enhances error details as intended by this PR.


13-13: Verify that executor exists before accessing result.

Line 13 accesses operatorPrompt.executor.result without checking if executor exists. The early return on line 11 ensures at least one of outputFields, executorError, or resolveError is truthy, but if only outputFields is truthy and no errors exist, executor might be undefined, leading to a runtime error.

app/packages/components/src/components/ErrorBoundary/ErrorBoundary.tsx (1)

76-78: LGTM! Conditional rendering improves clarity.

Adding the "Message" entry only when error.message exists is good defensive programming and improves the error display structure.

fiftyone/core/utils.py (2)

3197-3208: LGTM! Minimal NumPy type handling.

The Encoder class correctly handles NumPy scalar types during JSON serialization. The minimal approach is appropriate based on past discussions about keeping serialization case-by-case rather than attempting to handle all complex types.


3218-3218: LGTM! Lazy import for storage module.

The lazy import singleton is a good pattern for commonly used modules and is already being utilized elsewhere in the codebase (e.g., line 2039).

fiftyone/server/decorators.py (1)

19-19: LGTM! Consolidates response creation.

The change successfully consolidates response creation logic to fiftyone.core.utils, reducing code duplication while maintaining the same behavior.

Also applies to: 37-37

@jleven jleven merged commit 6653df7 into release/v1.9.0 Oct 16, 2025
18 checks passed
@jleven jleven deleted the bugfix/panel-event-return branch October 16, 2025 20:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants