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,11 +73,21 @@ 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 
6880        self ._content_type : StreamingContentType  =  content_type 
6981
82+         # Default to sanitizing until we know the app isn't sanitizing errors 
83+         if  on_error  ==  "auto" :
84+             on_error  =  "sanitize" 
85+             app  =  self ._session .app 
86+             if  app  is  not None  and  not  app .sanitize_errors :  # type: ignore 
87+                 on_error  =  "actual" 
88+ 
89+         self .on_error  =  on_error 
90+ 
7091        # TODO: remove the `None` when this PR lands: 
7192        # https://github.com/posit-dev/py-shiny/pull/793/files 
7293        self ._session  =  require_active_session (None )
@@ -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