From b8cf42dfa94052b85be235a96430de22ee6d1c9d Mon Sep 17 00:00:00 2001
From: Frost Ming <me@frostming.com>
Date: Fri, 27 Sep 2024 11:59:04 +0800
Subject: [PATCH 1/2] fix: expose errors occurring before the first yield of
 stream response

Signed-off-by: Frost Ming <me@frostming.com>
---
 src/_bentoml_sdk/io_models.py | 22 +++++++++++++++++++++-
 1 file changed, 21 insertions(+), 1 deletion(-)

diff --git a/src/_bentoml_sdk/io_models.py b/src/_bentoml_sdk/io_models.py
index f21713f531e..9fd94dda7f3 100644
--- a/src/_bentoml_sdk/io_models.py
+++ b/src/_bentoml_sdk/io_models.py
@@ -173,6 +173,7 @@ async def from_http_request(cls, request: Request, serde: Serde) -> IODescriptor
     @classmethod
     async def to_http_response(cls, obj: t.Any, serde: Serde) -> Response:
         """Convert a output value to HTTP response"""
+        import itertools
         import mimetypes
 
         from pydantic import RootModel
@@ -183,10 +184,24 @@ async def to_http_response(cls, obj: t.Any, serde: Serde) -> Response:
         from _bentoml_impl.serde import JSONSerde
 
         if inspect.isasyncgen(obj):
+            try:
+                # try if there is any error before the first yield
+                # if so, the error can be surfaced in the response
+                first_chunk = await obj.__anext__()
+            except StopAsyncIteration:
+                pre_chunks = []
+            else:
+                pre_chunks = [first_chunk]
+
+            async def gen() -> t.AsyncGenerator[t.Any, None]:
+                for chunk in pre_chunks:
+                    yield chunk
+                async for chunk in obj:
+                    yield chunk
 
             async def async_stream() -> t.AsyncGenerator[str | bytes, None]:
                 try:
-                    async for item in obj:
+                    async for item in gen():
                         if isinstance(item, (str, bytes)):
                             yield item
                         else:
@@ -201,6 +216,11 @@ async def async_stream() -> t.AsyncGenerator[str | bytes, None]:
             return StreamingResponse(async_stream(), media_type=cls.mime_type())
 
         elif inspect.isgenerator(obj):
+            trial, obj = itertools.tee(obj)
+            try:
+                next(trial)  # try if there is any error before the first yield
+            except StopIteration:
+                pass
 
             def content_stream() -> t.Generator[str | bytes, None, None]:
                 try:

From 08935a8ed4f785d682a090c4d9465dfc8d954b0c Mon Sep 17 00:00:00 2001
From: Frost Ming <me@frostming.com>
Date: Fri, 27 Sep 2024 17:32:10 +0800
Subject: [PATCH 2/2] fix: log endpoint and console url when developing

Signed-off-by: Frost Ming <me@frostming.com>
---
 src/bentoml/_internal/cloud/deployment.py | 13 ++++++++-----
 1 file changed, 8 insertions(+), 5 deletions(-)

diff --git a/src/bentoml/_internal/cloud/deployment.py b/src/bentoml/_internal/cloud/deployment.py
index f14837dc953..ef7e664f353 100644
--- a/src/bentoml/_internal/cloud/deployment.py
+++ b/src/bentoml/_internal/cloud/deployment.py
@@ -832,6 +832,8 @@ def is_bento_changed(bento_info: Bento) -> bool:
 
         spinner = Spinner(console=console)
         needs_update = is_bento_changed(bento_info)
+        spinner.log(f"💻 View Dashboard: {self.admin_console}")
+        endpoint_url: str | None = None
         try:
             spinner.start()
             upload_id = spinner.transmission_progress.add_task(
@@ -861,6 +863,9 @@ def is_bento_changed(bento_info: Bento) -> bool:
                     requirements_hash = self._init_deployment_files(
                         bento_dir, spinner=spinner
                     )
+                if endpoint_url is None:
+                    endpoint_url = self.get_endpoint_urls(False)[0]
+                    spinner.log(f"🌐 Endpoint: {endpoint_url}")
                 with self._tail_logs(spinner.console):
                     spinner.update("👀 Watching for changes")
                     for changes in watchfiles.watch(
@@ -937,13 +942,11 @@ def is_bento_changed(bento_info: Bento) -> bool:
                             return
         except KeyboardInterrupt:
             spinner.log(
-                "The deployment is still running, view the dashboard:\n"
-                f"    [blue]{self.admin_console}[/]\n\n"
-                "Next steps:\n"
-                "* Push the bento to BentoCloud:\n"
-                "    [blue]$ bentoml build --push[/]\n\n"
+                "\nWatcher stopped. Next steps:\n"
                 "* Attach to this deployment again:\n"
                 f"    [blue]$ bentoml develop --attach {self.name} --cluster {self.cluster}[/]\n\n"
+                "* Push the bento to BentoCloud:\n"
+                "    [blue]$ bentoml build --push[/]\n\n"
                 "* Terminate the deployment:\n"
                 f"    [blue]$ bentoml deployment terminate {self.name} --cluster {self.cluster}[/]"
             )