55from .._docstring import add_example
66from .._typing_extensions import TypedDict
77from ..session import require_active_session
8+ from ..types import NotifyException
89from . import Tag
910from ._html_deps_py_shiny import markdown_stream_dependency
1011
@@ -49,6 +50,16 @@ class MarkdownStream:
4950 - `"html"`: for rendering HTML content.
5051 - `"text"`: for plain text.
5152 - `"semi-markdown"`: for rendering markdown, but with HTML tags escaped.
53+ on_error
54+ How to handle errors that occur while streaming. When `"unhandled"`,
55+ the app will stop running when an error occurs. Otherwise, a notification
56+ is displayed to the user and the app continues to run.
57+
58+ * `"auto"`: Sanitize the error message if the app is set to sanitize errors,
59+ otherwise display the actual error message.
60+ * `"actual"`: Display the actual error message to the user.
61+ * `"sanitize"`: Sanitize the error message before displaying it to the user.
62+ * `"unhandled"`: Do not display any error message to the user.
5263
5364 Note
5465 ----
@@ -62,6 +73,7 @@ def __init__(
6273 * ,
6374 content : str = "" ,
6475 content_type : StreamingContentType = "markdown" ,
76+ on_error : Literal ["auto" , "actual" , "sanitize" , "unhandled" ] = "auto" ,
6577 ):
6678 self .id = id
6779 self ._content = content
@@ -71,6 +83,15 @@ def __init__(
7183 # https://github.com/posit-dev/py-shiny/pull/793/files
7284 self ._session = require_active_session (None )
7385
86+ # Default to sanitizing until we know the app isn't sanitizing errors
87+ if on_error == "auto" :
88+ on_error = "sanitize"
89+ app = self ._session .app
90+ if app is not None and not app .sanitize_errors : # type: ignore
91+ on_error = "actual"
92+
93+ self .on_error = on_error
94+
7495 def ui (self ) -> Tag :
7596 """
7697 Get the UI element for this markdown stream.
@@ -109,6 +130,15 @@ async def _task():
109130
110131 _task ()
111132
133+ # Since the task runs in the background (outside/beyond the current context,
134+ # if any), we need to manually raise any exceptions that occur
135+ @reactive .effect
136+ async def _handle_error ():
137+ e = _task .error ()
138+ if e :
139+ await self ._raise_exception (e )
140+ _handle_error .destroy () # type: ignore
141+
112142 def _append (self , content : str ):
113143 msg : ContentMessage = {
114144 "id" : self .id ,
@@ -144,6 +174,13 @@ def _streaming_dot(self):
144174 }
145175 self ._send_custom_message (end )
146176
177+ async def _raise_exception (self , e : BaseException ):
178+ if self .on_error == "unhandled" :
179+ raise e
180+ else :
181+ sanitize = self .on_error == "sanitize"
182+ raise NotifyException (str (e ), sanitize = sanitize ) from e
183+
147184 def _send_custom_message (self , msg : Union [ContentMessage , isStreamingMessage ]):
148185 if self ._session .is_stub_session ():
149186 return
0 commit comments